diff --git a/fleetd/src/main/java/dev/ltms/fleet/member/EnvAllowListScrub.java b/fleetd/src/main/java/dev/ltms/fleet/member/EnvAllowListScrub.java index d56105b..6b47d0b 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/member/EnvAllowListScrub.java +++ b/fleetd/src/main/java/dev/ltms/fleet/member/EnvAllowListScrub.java @@ -334,7 +334,30 @@ public final class EnvAllowListScrub { _cb633_blank+=("$_cb633_n") done - { for _cb633_n in "${_cb633_blank[@]}"; do export "$_cb633_n="; done; } 2>/dev/null + # `export UID=` is not a failed command: it is a FATAL zsh parameter error + # ("failed to change user ID") that aborts this whole sourced file mid-loop, + # leaving every later name unscrubbed and the report below unwritten — silently, + # because of the 2>/dev/null. Neither `|| true` nor a `${(t)n}` type guard + # contains it; only `eval` does. `eval` is safe here precisely because the loop + # above already rejected every name that is not [A-Za-z_][A-Za-z0-9_]*, so + # nothing but a bare identifier can reach it. + # + # Enumerating the special names instead (UID|EUID|GID|EGID|PPID|LINENO) also + # works, but only for the ones enumerated: a special that turns up exported on + # some other host brings the abort straight back. `eval` contains all of them. + # + # Then VERIFY. A contained failure is still a failure, so a name that did not + # actually blank must not be reported as blanked. It currently falls into the + # "allowed" count, which is imprecise in the safe direction; the honest third + # count ("tried and could not blank") needs a report-format change and belongs + # with fleetd #394, not here. + typeset -a _cb633_done + _cb633_done=() + for _cb633_n in "${_cb633_blank[@]}"; do + eval "export ${_cb633_n}=" 2>/dev/null + [[ -z "${(P)_cb633_n}" ]] && _cb633_done+=("$_cb633_n") + done + _cb633_blank=("${_cb633_done[@]}") integer _cb633_kept=$(( _cb633_total - ${#_cb633_blank} )) { @@ -342,7 +365,7 @@ public final class EnvAllowListScrub { for _cb633_n in "${_cb633_blank[@]}"; do print -r -- "$_cb633_n"; done } > "$ZDOTDIR/%s" 2>/dev/null - unset _cb633_allowed _cb633_names _cb633_blank _cb633_n _cb633_total _cb633_kept + unset _cb633_done _cb633_allowed _cb633_names _cb633_blank _cb633_n _cb633_total _cb633_kept """.formatted(names, MemberEnvAllowList.zshCasePattern(), REPORT_FILE); } diff --git a/fleetd/src/test/java/dev/ltms/fleet/member/EnvAllowListScrubTest.java b/fleetd/src/test/java/dev/ltms/fleet/member/EnvAllowListScrubTest.java index 6d16bba..55a44b3 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/member/EnvAllowListScrubTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/member/EnvAllowListScrubTest.java @@ -18,6 +18,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -118,6 +119,43 @@ class EnvAllowListScrubTest { "allowed N of M with N <= M — the denominator is always reported"); } + /** + * A pane inherits {@code UID}; a cleared test parent does not. The scrub must survive it. + * + *
Every other test here starts zsh from a CLEARED environment, so {@code UID} is never an + * exported name and never reaches the blanking loop. In a real member pane it is exported and + * it IS reached — and {@code export UID=} is a fatal zsh parameter error that aborts the whole + * sourced file, leaving every later name unscrubbed and writing no report at all. The abort is + * silent: the loop is wrapped in {@code 2>/dev/null}. + * + *
The assertion is deliberately "a report exists" rather than "the canary is blanked". The
+ * report is written by the last statement in the file, so its presence proves the script ran
+ * to completion; the canary alone would depend on where it happens to sit in {@code env} order.
+ * Both are checked, but only the first one fails deterministically without the fix.
+ */
+ @Test
+ void scrubSurvivesAnInheritedUidTheWayARealPaneHasIt(@TempDir Path tmp) throws Exception {
+ assumeTrue(Files.isExecutable(ZSH), "/bin/zsh not present — nothing to prove here");
+ Set