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 280636d..6938115 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/member/EnvAllowListScrub.java +++ b/fleetd/src/main/java/dev/ltms/fleet/member/EnvAllowListScrub.java @@ -105,9 +105,9 @@ public final class EnvAllowListScrub { reapOrphans(parentDir); Path dir = Files.createTempDirectory(parentDir, DIR_PREFIX); dir.toFile().deleteOnExit(); - // The report is written by zsh, after these hooks are registered, so register its path - // too — otherwise the directory is non-empty at JVM exit and cannot be removed at all. - dir.resolve(REPORT_FILE).toFile().deleteOnExit(); + // zsh truncates this pre-created receipt after these hooks are registered. Register its + // path too — otherwise the directory is non-empty at JVM exit and cannot be removed. + Files.createFile(dir.resolve(REPORT_FILE)).toFile().deleteOnExit(); write(dir, SCRUB_FILE, scrubScript(allowedNames)); write(dir, ".zshenv", homeSourcingFile(".zshenv")); write(dir, ".zprofile", homeSourcingFile(".zprofile")); @@ -147,12 +147,11 @@ public final class EnvAllowListScrub { * into it: owner keeps full access, {@code group} gets traverse+read on the directory ({@code * rwxr-x---}, so a member process — a login shell reading it via {@code ZDOTDIR}, or another * process simply opening a file under it — running under that group can find and read the - * files) and read-only on each file ({@code rw-r-----}) — deliberately no group WRITE anywhere, - * since a member never needs to add or change what fleetd generated. (For the ZDOTDIR scrub - * specifically, this also means the scrub script's own report write inside the pane fails - * closed rather than open — see {@code scrub.zsh}'s trailing {@code 2>/dev/null} — which {@link - * dev.ltms.fleet.member.HerdrPeerLauncher#releaseZdotdir} already treats as "cannot be - * confirmed to have run" rather than success.) + * files) and read-only on each file ({@code rw-r-----}), except the pre-created ZDOTDIR + * {@code scrub-report.txt}. That receipt gets group write ({@code rw-rw----}), so + * {@code scrub.zsh} can truncate and write it without granting group write on the directory. + * If its optional permission change fails, the member cannot write a receipt and the launcher + * keeps its existing WARN rather than failing the spawn. * *

Package-private and named generically on purpose: fleetd #213 built this for the ZDOTDIR * scrub directory, and fleetd #219 reuses it verbatim for {@link @@ -167,7 +166,15 @@ public final class EnvAllowListScrub { setGroupAndPermissions(dir, principal, "rwxr-x---"); try (Stream entries = Files.list(dir)) { for (Path file : entries.toList()) { - setGroupAndPermissions(file, principal, "rw-r-----"); + if (REPORT_FILE.equals(file.getFileName().toString())) { + try { + setGroupAndPermissions(file, principal, "rw-rw----"); + } catch (IOException | UnsupportedOperationException ignored) { + // The receipt is optional. Its absence keeps the existing WARN path. + } + } else { + setGroupAndPermissions(file, principal, "rw-r-----"); + } } } } catch (IOException e) { 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 d1534e0..be4125e 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/member/EnvAllowListScrubTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/member/EnvAllowListScrubTest.java @@ -108,6 +108,31 @@ class EnvAllowListScrubTest { "allowed N of M with N <= M — the denominator is always reported"); } + /** A group-shared ZDOTDIR still lets the member truncate and write its pre-created receipt. */ + @Test + void groupSharedScrubWritesAndReadsItsReport(@TempDir Path tmp) throws Exception { + assumeTrue(Files.isExecutable(ZSH), "/bin/zsh not present — nothing to prove here"); + Set allowed = MemberEnvAllowList.derive(List.of()); + Path zdotdir = EnvAllowListScrub.generate(tmp, allowed, currentUserGroup()); + + Map cleanParent = Map.of( + "HOME", System.getProperty("user.home"), + "PATH", "/usr/bin:/bin", + "SHELL", "/bin/zsh"); + exportedNamesFromCleanParent(cleanParent, zdotdir); + + EnvAllowListScrub.ScrubReport report = EnvAllowListScrub.readReport(zdotdir); + assertNotNull(report, "a group-shared completed login shell must leave a report behind"); + assertTrue(report.allowed() >= 0 && report.total() >= report.allowed(), + "allowed N of M with N <= M — the denominator is always reported"); + assertEquals("rw-rw----", java.nio.file.attribute.PosixFilePermissions.toString( + Files.getPosixFilePermissions(zdotdir.resolve(EnvAllowListScrub.REPORT_FILE))), + "the pre-created receipt must be group-writable"); + assertEquals("rwxr-x---", java.nio.file.attribute.PosixFilePermissions.toString( + Files.getPosixFilePermissions(zdotdir)), + "group sharing must not make the ZDOTDIR directory group-writable"); + } + /** Report parsing is lenient: absent file → null (no measurement), not an exception. */ @Test void readReportReturnsNullForADirectoryWithoutOne(@TempDir Path dir) {