From 97b09bdb82eb75d971f359da515826b2371da273 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 28 Aug 2026 08:29:43 +0200 Subject: [PATCH 1/5] Fix crash-tracking script/directory permissions and add regression tests Clear all permission bits before setting owner-only ones so stray group/other bits from the process umask are actually stripped, and fix the OOME notifier script lockdown to operate on the script file instead of the directory. --- .../CrashUploaderScriptInitializer.java | 5 ++ .../OOMENotifierScriptInitializer.java | 12 +++- .../ScriptInitializerSecurityTest.java | 56 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java index f2e7ac706bb..1a1e5b5c805 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java @@ -78,6 +78,11 @@ private static boolean copyCrashUploaderScript( scriptDirectory); return false; } + // first clear all privileges + scriptDirectory.setReadable(false, false); + scriptDirectory.setWritable(false, false); + scriptDirectory.setExecutable(false, false); + // then set them only for the owner scriptDirectory.setReadable(true, true); scriptDirectory.setWritable(true, true); scriptDirectory.setExecutable(true, true); diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java index 91420eda112..f5ff7aeba5e 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java @@ -86,6 +86,11 @@ private static boolean copyOOMEscript(File scriptFile) { scriptDirectory); return false; } + // first clear all privileges + scriptDirectory.setReadable(false, false); + scriptDirectory.setWritable(false, false); + scriptDirectory.setExecutable(false, false); + // then set them only for the owner scriptDirectory.setReadable(true, true); scriptDirectory.setWritable(true, true); scriptDirectory.setExecutable(true, true); @@ -95,8 +100,13 @@ private static boolean copyOOMEscript(File scriptFile) { // do not overwrite existing if (!scriptFile.exists()) { copyStream(getOomeNotifierTemplate(), scriptFile); - scriptFile.setReadable(true, true); + // first clear all privileges + scriptFile.setReadable(false, false); scriptFile.setWritable(false, false); + scriptFile.setExecutable(false, false); + // then set them only for the owner + scriptFile.setReadable(true, true); + // do not restore the writable scriptFile.setExecutable(true, true); } else { if (!isOwnedAndPrivate(scriptFile)) { diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java index 8f0cc003948..4490caf3428 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java @@ -1,6 +1,7 @@ package datadog.crashtracking; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -12,6 +13,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.Comparator; +import java.util.EnumSet; import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; @@ -108,6 +110,46 @@ void crashUploaderHijackedDirectoryIsRefused() throws Exception { assertFalse(Files.exists(scriptFile), "Script must not be written into a hijacked directory"); } + @Test + void crashUploaderFreshDirHasExactlyOwnerPermissions() throws Exception { + Path scriptFile = tempDir.resolve("dd_crash_uploader.sh"); + CrashUploaderScriptInitializer.initialize(scriptFile.toString(), "/tmp/hs_err.log"); + + // The directory is created by mkdirs() (subject to the process umask) before the + // owner-only bits are applied, so any stray group/other bits left over from that + // umask must have been cleared, not just overlaid with the owner bits. + assertPermissions( + scriptFile.getParent(), + EnumSet.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE)); + } + + @Test + void oomeNotifierFreshDirHasExactlyOwnerPermissions() throws Exception { + Path scriptFile = tempDir.resolve("dd_oome_notifier.sh"); + OOMENotifierScriptInitializer.initialize(scriptFile + " %p"); + + assertPermissions( + scriptFile.getParent(), + EnumSet.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE)); + } + + @Test + void oomeNotifierScriptFileIsNotOwnerWritable() throws Exception { + Path scriptFile = tempDir.resolve("dd_oome_notifier.sh"); + OOMENotifierScriptInitializer.initialize(scriptFile + " %p"); + + // The write bit is deliberately not restored after the clear/set-owner-only sequence, + // so the copied script must end up read+execute only, even for the owner. + assertPermissions( + scriptFile, EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_EXECUTE)); + } + @Test void oomeNotifierFreshDirIsOwnerRestricted() throws Exception { Path scriptFile = tempDir.resolve("dd_oome_notifier.sh"); @@ -176,6 +218,20 @@ void cleanPosixTreeEndToEndInitProducesScriptsAndConfigs() throws Exception { assertTrue(crashCfgWritten, "Crash uploader .cfg file must be written in the clean flow"); } + private static void assertPermissions(Path path, Set expected) + throws IOException { + Set actual = Files.getPosixFilePermissions(path); + assertEquals( + actual, + expected, + "Expected permissions " + + PosixFilePermissions.toString(expected) + + " but found " + + PosixFilePermissions.toString(actual) + + " on " + + path); + } + private static void assertNoGroupWorldWriteBit(Path path) throws IOException { Set perms = Files.getPosixFilePermissions(path); for (PosixFilePermission bit : From 1327f248fa95293908ee12c2c6bb3aea7b53049c Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 28 Aug 2026 10:35:24 +0200 Subject: [PATCH 2/5] sphinx: address review feedback on PR #12330 --- .../CrashUploaderScriptInitializer.java | 25 ++++---- .../datadog/crashtracking/Initializer.java | 57 ++++++++++++++++- .../OOMENotifierScriptInitializer.java | 16 +++-- .../ScriptInitializerSecurityTest.java | 62 ++++++++++++++++++- 4 files changed, 135 insertions(+), 25 deletions(-) diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java index 1a1e5b5c805..68e1977b44d 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java @@ -5,6 +5,8 @@ import static datadog.crashtracking.Initializer.findAgentJar; import static datadog.crashtracking.Initializer.getCrashUploaderTemplate; import static datadog.crashtracking.Initializer.isOwnedAndPrivate; +import static datadog.crashtracking.Initializer.isSafeToRepairDirectory; +import static datadog.crashtracking.Initializer.restrictDirectoryToOwnerOnly; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import static java.util.Locale.ROOT; @@ -78,23 +80,19 @@ private static boolean copyCrashUploaderScript( scriptDirectory); return false; } - // first clear all privileges - scriptDirectory.setReadable(false, false); - scriptDirectory.setWritable(false, false); - scriptDirectory.setExecutable(false, false); - // then set them only for the owner - scriptDirectory.setReadable(true, true); - scriptDirectory.setWritable(true, true); - scriptDirectory.setExecutable(true, true); + restrictDirectoryToOwnerOnly(scriptDirectory); } else { - if (!isOwnedAndPrivate(scriptDirectory)) { + if (!isSafeToRepairDirectory(scriptDirectory)) { LOG.warn( SEND_TELEMETRY, - "Untrusted crash tracking script folder {} (wrong owner or group/world bits set). " + "Untrusted crash tracking script folder {} (wrong owner or group/world-writable). " + SETUP_FAILURE_MESSAGE, scriptDirectory); return false; } + // owned by us but possibly left over from an older, less restrictive version: tighten it + // down to owner-only before trusting it, then validate the script inside it independently + restrictDirectoryToOwnerOnly(scriptDirectory); } if (!scriptDirectory.canWrite()) { LOG.warn(SEND_TELEMETRY, "Read only directory {}. " + SETUP_FAILURE_MESSAGE, scriptDirectory); @@ -142,8 +140,13 @@ private static void writeCrashUploaderScript( bw.newLine(); } } - scriptFile.setReadable(true, true); + // first clear all privileges + scriptFile.setReadable(false, false); scriptFile.setWritable(false, false); + scriptFile.setExecutable(false, false); + // then set them only for the owner + scriptFile.setReadable(true, true); + // do not restore the writable scriptFile.setExecutable(true, true); } else { if (!isOwnedAndPrivate(scriptFile)) { diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java index 8e1ba25f423..e8dc74373b2 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java @@ -460,9 +460,7 @@ static boolean isOwnedAndPrivate(File f) { } try { Path path = f.toPath(); - UserPrincipal owner = Files.getOwner(path); - UserPrincipal jvmUser = Files.getOwner(TempLocationManager.getInstance().getTempDir()); - if (!jvmUser.equals(owner)) { + if (!isJvmOwner(path)) { return false; } Set perms = Files.getPosixFilePermissions(path); @@ -472,4 +470,57 @@ static boolean isOwnedAndPrivate(File f) { return false; } } + + private static final Set GROUP_WORLD_WRITE_BITS = + EnumSet.of(PosixFilePermission.GROUP_WRITE, PosixFilePermission.OTHERS_WRITE); + + /** + * Returns {@code true} when {@code dir} is owned by the current JVM user and has no group/world + * write bit set; on non-POSIX file systems always returns {@code true}. Unlike {@link + * #isOwnedAndPrivate(File)}, stray group/world read or execute bits (e.g. the + * {@code 0755} a pre-upgrade version of this initializer, which did not lock down permissions, + * could have left behind) do not disqualify the directory here: those bits are safe to tighten in + * place with {@link #restrictDirectoryToOwnerOnly(File)} rather than treating the directory as + * untrusted. A group/world write bit is still treated as a sign of possible tampering and causes + * this method to return {@code false}. + */ + static boolean isSafeToRepairDirectory(File dir) { + if (OperatingSystem.isWindows()) { + return true; + } + try { + Path path = dir.toPath(); + if (!isJvmOwner(path)) { + return false; + } + Set perms = Files.getPosixFilePermissions(path); + return perms.stream().noneMatch(GROUP_WORLD_WRITE_BITS::contains); + } catch (IOException | IllegalStateException e) { + LOG.debug("Unable to check ownership/permissions for {}: {}", dir, e.getMessage()); + return false; + } + } + + private static boolean isJvmOwner(Path path) throws IOException { + UserPrincipal owner = Files.getOwner(path); + UserPrincipal jvmUser = Files.getOwner(TempLocationManager.getInstance().getTempDir()); + return jvmUser.equals(owner); + } + + /** + * Clears all permission bits on {@code dir} and then sets read/write/execute for the owner only + * (effective {@code 0700}). Used both when a script directory is freshly created (to strip any + * group/world bits left over from the process umask) and to repair a pre-existing directory that + * is owned by the JVM user but was created by an older, less restrictive version. + */ + static void restrictDirectoryToOwnerOnly(File dir) { + // first clear all privileges + dir.setReadable(false, false); + dir.setWritable(false, false); + dir.setExecutable(false, false); + // then set them only for the owner + dir.setReadable(true, true); + dir.setWritable(true, true); + dir.setExecutable(true, true); + } } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java index f5ff7aeba5e..ee52cfd68cf 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java @@ -6,7 +6,9 @@ import static datadog.crashtracking.Initializer.getOomeNotifierTemplate; import static datadog.crashtracking.Initializer.getScriptPathFromArg; import static datadog.crashtracking.Initializer.isOwnedAndPrivate; +import static datadog.crashtracking.Initializer.isSafeToRepairDirectory; import static datadog.crashtracking.Initializer.pidFromSpecialFileName; +import static datadog.crashtracking.Initializer.restrictDirectoryToOwnerOnly; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import datadog.trace.api.internal.VisibleForTesting; @@ -61,13 +63,16 @@ private static boolean copyOOMEscript(File scriptFile) { File scriptDirectory = scriptFile.getParentFile(); if (scriptDirectory.exists()) { - if (!isOwnedAndPrivate(scriptDirectory)) { + if (!isSafeToRepairDirectory(scriptDirectory)) { LOG.warn( SEND_TELEMETRY, "Untrusted OOME script folder {} (wrong owner or group/world bits set). OOME notification will not work properly.", scriptDirectory); return false; } + // owned by us but possibly left over from an older, less restrictive version: tighten it + // down to owner-only before trusting it, then validate the script inside it independently + restrictDirectoryToOwnerOnly(scriptDirectory); // cleanup all stale process-specific generated files in the parent folder of the given OOME // notifier script runScriptCleanup(scriptDirectory); @@ -86,14 +91,7 @@ private static boolean copyOOMEscript(File scriptFile) { scriptDirectory); return false; } - // first clear all privileges - scriptDirectory.setReadable(false, false); - scriptDirectory.setWritable(false, false); - scriptDirectory.setExecutable(false, false); - // then set them only for the owner - scriptDirectory.setReadable(true, true); - scriptDirectory.setWritable(true, true); - scriptDirectory.setExecutable(true, true); + restrictDirectoryToOwnerOnly(scriptDirectory); } try { diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java index 4490caf3428..625e15031ea 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java @@ -110,9 +110,51 @@ void crashUploaderHijackedDirectoryIsRefused() throws Exception { assertFalse(Files.exists(scriptFile), "Script must not be written into a hijacked directory"); } + @Test + void crashUploaderRepairsPreviouslyGeneratedDirectory() throws Exception { + // Simulate a directory left behind by a pre-upgrade version of the initializer that did not + // lock down permissions: owned by us, but group/world readable+executable (0755) with no + // write bits set for group/other. + Path scriptDir = tempDir.resolve("legacy_crash_dir"); + Files.createDirectories(scriptDir); + Files.setPosixFilePermissions(scriptDir, PosixFilePermissions.fromString("rwxr-xr-x")); + + Path scriptFile = scriptDir.resolve("dd_crash_uploader.sh"); + CrashUploaderScriptInitializer.initialize(scriptFile.toString(), "/tmp/hs_err.log"); + + assertTrue(Files.exists(scriptFile), "Script should have been created in the repaired dir"); + assertPermissions( + scriptDir, + EnumSet.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE)); + } + + @Test + void oomeNotifierRepairsPreviouslyGeneratedDirectory() throws Exception { + Path scriptDir = tempDir.resolve("legacy_oome_dir"); + Files.createDirectories(scriptDir); + Files.setPosixFilePermissions(scriptDir, PosixFilePermissions.fromString("rwxr-xr-x")); + + Path scriptFile = scriptDir.resolve("dd_oome_notifier.sh"); + OOMENotifierScriptInitializer.initialize(scriptFile + " %p"); + + assertTrue(Files.exists(scriptFile), "Script should have been created in the repaired dir"); + assertPermissions( + scriptDir, + EnumSet.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE)); + } + @Test void crashUploaderFreshDirHasExactlyOwnerPermissions() throws Exception { - Path scriptFile = tempDir.resolve("dd_crash_uploader.sh"); + // Place the script under a child directory that does not exist yet, so the initializer + // must go through its mkdirs()/permission-reset branch rather than the existing-directory + // (already owner-only) branch that tempDir itself would take. + Path scriptFile = tempDir.resolve("fresh-crash-dir").resolve("dd_crash_uploader.sh"); CrashUploaderScriptInitializer.initialize(scriptFile.toString(), "/tmp/hs_err.log"); // The directory is created by mkdirs() (subject to the process umask) before the @@ -128,7 +170,10 @@ void crashUploaderFreshDirHasExactlyOwnerPermissions() throws Exception { @Test void oomeNotifierFreshDirHasExactlyOwnerPermissions() throws Exception { - Path scriptFile = tempDir.resolve("dd_oome_notifier.sh"); + // Place the script under a child directory that does not exist yet, so the initializer + // must go through its mkdirs()/permission-reset branch rather than the existing-directory + // (already owner-only) branch that tempDir itself would take. + Path scriptFile = tempDir.resolve("fresh-oome-dir").resolve("dd_oome_notifier.sh"); OOMENotifierScriptInitializer.initialize(scriptFile + " %p"); assertPermissions( @@ -139,6 +184,19 @@ void oomeNotifierFreshDirHasExactlyOwnerPermissions() throws Exception { PosixFilePermission.OWNER_EXECUTE)); } + @Test + void crashUploaderScriptFileHasNoGroupOrWorldReadBit() throws Exception { + Path scriptFile = tempDir.resolve("dd_crash_uploader.sh"); + CrashUploaderScriptInitializer.initialize(scriptFile.toString(), "/tmp/hs_err.log"); + + // The script is created with FileOutputStream, so its initial mode is subject to the + // process umask (e.g. 0644 under a typical 0022 umask). The clear-then-set-owner-only + // sequence must strip any inherited group/other bits, not just overlay owner bits on top + // of them, otherwise a later JVM start rejects the script via isOwnedAndPrivate(). + assertPermissions( + scriptFile, EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_EXECUTE)); + } + @Test void oomeNotifierScriptFileIsNotOwnerWritable() throws Exception { Path scriptFile = tempDir.resolve("dd_oome_notifier.sh"); From 766939b374eb864c8a54fbd15bf63f3fa116ece5 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 28 Aug 2026 10:48:24 +0200 Subject: [PATCH 3/5] Spotless!!! --- .../CrashUploaderScriptInitializer.java | 8 ++++--- .../datadog/crashtracking/Initializer.java | 21 +++++++++++++++++++ .../OOMENotifierScriptInitializer.java | 8 ++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java index 68e1977b44d..bd57687d643 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java @@ -7,6 +7,7 @@ import static datadog.crashtracking.Initializer.isOwnedAndPrivate; import static datadog.crashtracking.Initializer.isSafeToRepairDirectory; import static datadog.crashtracking.Initializer.restrictDirectoryToOwnerOnly; +import static datadog.crashtracking.Initializer.stripGroupAndWorldBits; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import static java.util.Locale.ROOT; @@ -90,9 +91,10 @@ private static boolean copyCrashUploaderScript( scriptDirectory); return false; } - // owned by us but possibly left over from an older, less restrictive version: tighten it - // down to owner-only before trusting it, then validate the script inside it independently - restrictDirectoryToOwnerOnly(scriptDirectory); + // owned by us but possibly left over from an older, less restrictive version: strip any + // stray group/world bits without touching the owner's own bits, so a directory an operator + // deliberately made non-writable stays non-writable + stripGroupAndWorldBits(scriptDirectory); } if (!scriptDirectory.canWrite()) { LOG.warn(SEND_TELEMETRY, "Read only directory {}. " + SETUP_FAILURE_MESSAGE, scriptDirectory); diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java index e8dc74373b2..228973a4378 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java @@ -523,4 +523,25 @@ static void restrictDirectoryToOwnerOnly(File dir) { dir.setWritable(true, true); dir.setExecutable(true, true); } + + /** + * Removes any group/world permission bits from {@code dir} while leaving the owner's own bits + * untouched. Unlike {@link #restrictDirectoryToOwnerOnly(File)}, this never adds a permission + * (e.g. owner write) that the directory did not already have, so a directory an operator + * deliberately made non-writable for the owner stays non-writable after repair. On non-POSIX + * file systems this is a no-op. + */ + static void stripGroupAndWorldBits(File dir) { + if (OperatingSystem.isWindows()) { + return; + } + try { + Path path = dir.toPath(); + Set perms = EnumSet.copyOf(Files.getPosixFilePermissions(path)); + perms.removeAll(GROUP_WORLD_BITS); + Files.setPosixFilePermissions(path, perms); + } catch (IOException | IllegalStateException e) { + LOG.debug("Unable to strip group/world permissions for {}: {}", dir, e.getMessage()); + } + } } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java index ee52cfd68cf..e4ae6fda673 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java @@ -9,6 +9,7 @@ import static datadog.crashtracking.Initializer.isSafeToRepairDirectory; import static datadog.crashtracking.Initializer.pidFromSpecialFileName; import static datadog.crashtracking.Initializer.restrictDirectoryToOwnerOnly; +import static datadog.crashtracking.Initializer.stripGroupAndWorldBits; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import datadog.trace.api.internal.VisibleForTesting; @@ -70,9 +71,10 @@ private static boolean copyOOMEscript(File scriptFile) { scriptDirectory); return false; } - // owned by us but possibly left over from an older, less restrictive version: tighten it - // down to owner-only before trusting it, then validate the script inside it independently - restrictDirectoryToOwnerOnly(scriptDirectory); + // owned by us but possibly left over from an older, less restrictive version: strip any + // stray group/world bits without touching the owner's own bits, so a directory an operator + // deliberately made non-writable stays non-writable + stripGroupAndWorldBits(scriptDirectory); // cleanup all stale process-specific generated files in the parent folder of the given OOME // notifier script runScriptCleanup(scriptDirectory); From f1e6f66c8e083e5025ea115f9080e25f88f0d726 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 28 Aug 2026 12:44:20 +0200 Subject: [PATCH 4/5] Reformat comment in Initializer.stripGroupAndWorldBits --- .../src/main/java/datadog/crashtracking/Initializer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java index 228973a4378..d0f7cac1ad8 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java @@ -528,8 +528,8 @@ static void restrictDirectoryToOwnerOnly(File dir) { * Removes any group/world permission bits from {@code dir} while leaving the owner's own bits * untouched. Unlike {@link #restrictDirectoryToOwnerOnly(File)}, this never adds a permission * (e.g. owner write) that the directory did not already have, so a directory an operator - * deliberately made non-writable for the owner stays non-writable after repair. On non-POSIX - * file systems this is a no-op. + * deliberately made non-writable for the owner stays non-writable after repair. On non-POSIX file + * systems this is a no-op. */ static void stripGroupAndWorldBits(File dir) { if (OperatingSystem.isWindows()) { From 3a636763c7addff12d5ff8f46a804b955db78bf0 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Wed, 2 Sep 2026 11:29:46 +0200 Subject: [PATCH 5/5] Repair stale script files, fail closed on permission errors, atomic POSIX perms --- .../CrashUploaderScriptInitializer.java | 53 ++++--- .../datadog/crashtracking/Initializer.java | 146 +++++++++++------- .../OOMENotifierScriptInitializer.java | 53 +++++-- .../ScriptInitializerSecurityTest.java | 43 +++++- 4 files changed, 198 insertions(+), 97 deletions(-) diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java index bd57687d643..a4a9207ea81 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderScriptInitializer.java @@ -4,9 +4,9 @@ import static datadog.crashtracking.Initializer.LOG; import static datadog.crashtracking.Initializer.findAgentJar; import static datadog.crashtracking.Initializer.getCrashUploaderTemplate; -import static datadog.crashtracking.Initializer.isOwnedAndPrivate; -import static datadog.crashtracking.Initializer.isSafeToRepairDirectory; +import static datadog.crashtracking.Initializer.isSafeToRepair; import static datadog.crashtracking.Initializer.restrictDirectoryToOwnerOnly; +import static datadog.crashtracking.Initializer.restrictScriptToOwnerOnly; import static datadog.crashtracking.Initializer.stripGroupAndWorldBits; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; import static java.util.Locale.ROOT; @@ -81,9 +81,16 @@ private static boolean copyCrashUploaderScript( scriptDirectory); return false; } - restrictDirectoryToOwnerOnly(scriptDirectory); + if (!restrictDirectoryToOwnerOnly(scriptDirectory)) { + LOG.warn( + SEND_TELEMETRY, + "Unable to restrict crash tracking script folder {} to owner-only permissions. " + + SETUP_FAILURE_MESSAGE, + scriptDirectory); + return false; + } } else { - if (!isSafeToRepairDirectory(scriptDirectory)) { + if (!isSafeToRepair(scriptDirectory)) { LOG.warn( SEND_TELEMETRY, "Untrusted crash tracking script folder {} (wrong owner or group/world-writable). " @@ -94,7 +101,14 @@ private static boolean copyCrashUploaderScript( // owned by us but possibly left over from an older, less restrictive version: strip any // stray group/world bits without touching the owner's own bits, so a directory an operator // deliberately made non-writable stays non-writable - stripGroupAndWorldBits(scriptDirectory); + if (!stripGroupAndWorldBits(scriptDirectory)) { + LOG.warn( + SEND_TELEMETRY, + "Unable to strip group/world permissions from crash tracking script folder {}. " + + SETUP_FAILURE_MESSAGE, + scriptDirectory); + return false; + } } if (!scriptDirectory.canWrite()) { LOG.warn(SEND_TELEMETRY, "Read only directory {}. " + SETUP_FAILURE_MESSAGE, scriptDirectory); @@ -106,7 +120,8 @@ private static boolean copyCrashUploaderScript( } catch (UntrustedScriptException e) { LOG.warn( SEND_TELEMETRY, - "Untrusted crash uploader script {} (wrong owner or group/world-writable). " + "Untrusted or unprotectable crash uploader script {} (wrong owner, group/world-writable," + + " or unable to restrict permissions). " + SETUP_FAILURE_MESSAGE, scriptFile); return false; @@ -123,9 +138,14 @@ private static boolean copyCrashUploaderScript( static class UntrustedScriptException extends IOException {} /** - * Writes the crash uploader script if it does not already exist. When the script already exists - * it is validated for POSIX ownership and permissions before reuse; an untrusted script causes - * this method to throw {@link UntrustedScriptException} so the caller can return {@code false}. + * Writes the crash uploader script if it does not already exist. A freshly written script is + * immediately restricted to owner-only permissions; a script that cannot be locked down is + * discarded. When the script already exists it is validated for POSIX ownership and repairable + * permissions before reuse: a script owned by the JVM user without group/world write bits is + * repaired in place by stripping stray group/world bits (e.g. left behind by an older, less + * restrictive version of this initializer), anything else is untrusted. Failure to trust or + * repair the script causes this method to throw {@link UntrustedScriptException} so the caller + * can return {@code false}. */ private static void writeCrashUploaderScript( InputStream template, File scriptFile, String execClass, String crashFile) @@ -142,16 +162,13 @@ private static void writeCrashUploaderScript( bw.newLine(); } } - // first clear all privileges - scriptFile.setReadable(false, false); - scriptFile.setWritable(false, false); - scriptFile.setExecutable(false, false); - // then set them only for the owner - scriptFile.setReadable(true, true); - // do not restore the writable - scriptFile.setExecutable(true, true); + // fail closed: never leave a freshly written script we could not lock down + if (!restrictScriptToOwnerOnly(scriptFile)) { + scriptFile.delete(); + throw new UntrustedScriptException(); + } } else { - if (!isOwnedAndPrivate(scriptFile)) { + if (!isSafeToRepair(scriptFile) || !stripGroupAndWorldBits(scriptFile)) { throw new UntrustedScriptException(); } } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java index d0f7cac1ad8..f8b68e1c1a9 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java @@ -448,55 +448,40 @@ private static String getScriptFileName(String scriptName) { PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE); + private static final Set GROUP_WORLD_WRITE_BITS = + EnumSet.of(PosixFilePermission.GROUP_WRITE, PosixFilePermission.OTHERS_WRITE); + /** - * Returns {@code true} when {@code f} is safe to trust: on non-POSIX file systems always returns - * {@code true}; on POSIX returns {@code true} only when the path is owned by the current JVM user - * and has no group or world permission bits set (effective {@code 0700} for dirs, {@code 0600} or - * stricter for files). + * Returns {@code true} when {@code f} is owned by the current JVM user and has no group/world + * write bit set; on non-POSIX file systems always returns {@code true}. Stray + * group/world read or execute bits (e.g. the {@code 0755} a pre-upgrade version + * of this initializer, which did not lock down permissions, could have left behind) do not + * disqualify the path here: those bits are safe to tighten in place with {@link + * #stripGroupAndWorldBits(File)} rather than treating the path as untrusted. A group/world write + * bit is still treated as a sign of possible tampering and causes this method to return {@code + * false}. */ - static boolean isOwnedAndPrivate(File f) { - if (OperatingSystem.isWindows()) { - return true; - } - try { - Path path = f.toPath(); - if (!isJvmOwner(path)) { - return false; - } - Set perms = Files.getPosixFilePermissions(path); - return perms.stream().noneMatch(GROUP_WORLD_BITS::contains); - } catch (IOException | IllegalStateException e) { - LOG.debug("Unable to check ownership/permissions for {}: {}", f, e.getMessage()); - return false; - } + static boolean isSafeToRepair(File f) { + return isOwnedWithoutBits(f, GROUP_WORLD_WRITE_BITS); } - private static final Set GROUP_WORLD_WRITE_BITS = - EnumSet.of(PosixFilePermission.GROUP_WRITE, PosixFilePermission.OTHERS_WRITE); - /** - * Returns {@code true} when {@code dir} is owned by the current JVM user and has no group/world - * write bit set; on non-POSIX file systems always returns {@code true}. Unlike {@link - * #isOwnedAndPrivate(File)}, stray group/world read or execute bits (e.g. the - * {@code 0755} a pre-upgrade version of this initializer, which did not lock down permissions, - * could have left behind) do not disqualify the directory here: those bits are safe to tighten in - * place with {@link #restrictDirectoryToOwnerOnly(File)} rather than treating the directory as - * untrusted. A group/world write bit is still treated as a sign of possible tampering and causes - * this method to return {@code false}. + * Returns {@code true} when {@code f} is owned by the current JVM user and has none of {@code + * forbiddenBits} set. On non-POSIX file systems always returns {@code true}. */ - static boolean isSafeToRepairDirectory(File dir) { + private static boolean isOwnedWithoutBits(File f, Set forbiddenBits) { if (OperatingSystem.isWindows()) { return true; } try { - Path path = dir.toPath(); + Path path = f.toPath(); if (!isJvmOwner(path)) { return false; } Set perms = Files.getPosixFilePermissions(path); - return perms.stream().noneMatch(GROUP_WORLD_WRITE_BITS::contains); - } catch (IOException | IllegalStateException e) { - LOG.debug("Unable to check ownership/permissions for {}: {}", dir, e.getMessage()); + return perms.stream().noneMatch(forbiddenBits::contains); + } catch (IOException | IllegalStateException | UnsupportedOperationException e) { + LOG.debug("Unable to check ownership/permissions for {}: {}", f, e.getMessage()); return false; } } @@ -508,40 +493,83 @@ private static boolean isJvmOwner(Path path) throws IOException { } /** - * Clears all permission bits on {@code dir} and then sets read/write/execute for the owner only - * (effective {@code 0700}). Used both when a script directory is freshly created (to strip any - * group/world bits left over from the process umask) and to repair a pre-existing directory that - * is owned by the JVM user but was created by an older, less restrictive version. + * Sets read/write/execute for the owner only on a freshly created script directory (effective + * {@code 0700}, stripping any group/world bits left over from the process umask). Returns {@code + * true} when the permissions were applied; on failure the caller must treat the directory as + * unusable. */ - static void restrictDirectoryToOwnerOnly(File dir) { - // first clear all privileges - dir.setReadable(false, false); - dir.setWritable(false, false); - dir.setExecutable(false, false); - // then set them only for the owner - dir.setReadable(true, true); - dir.setWritable(true, true); - dir.setExecutable(true, true); + static boolean restrictDirectoryToOwnerOnly(File dir) { + return setOwnerOnlyPermissions(dir, true); } /** - * Removes any group/world permission bits from {@code dir} while leaving the owner's own bits - * untouched. Unlike {@link #restrictDirectoryToOwnerOnly(File)}, this never adds a permission - * (e.g. owner write) that the directory did not already have, so a directory an operator - * deliberately made non-writable for the owner stays non-writable after repair. On non-POSIX file - * systems this is a no-op. + * Sets read/execute (but not write) for the owner only on a freshly created script file. Returns + * {@code true} when the permissions were applied; on failure the caller must discard the file. */ - static void stripGroupAndWorldBits(File dir) { + static boolean restrictScriptToOwnerOnly(File scriptFile) { + return setOwnerOnlyPermissions(scriptFile, false); + } + + /** + * Applies owner-only permissions to {@code f}: all group/world bits are cleared and the owner + * keeps read and execute, plus write iff {@code ownerWritable}. On POSIX file systems this is a + * single atomic operation whose failure makes the caller refuse the path. On file systems without + * POSIX permission bits (e.g. Windows) there is nothing to verify, so restriction is best-effort + * through the legacy API and the method returns {@code true} even on partial failure. + */ + private static boolean setOwnerOnlyPermissions(File f, boolean ownerWritable) { + Path path = f.toPath(); + if (path.getFileSystem().supportedFileAttributeViews().contains("posix")) { + try { + Set perms = + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_EXECUTE); + if (ownerWritable) { + perms.add(PosixFilePermission.OWNER_WRITE); + } + Files.setPosixFilePermissions(path, perms); + return true; + } catch (IOException | IllegalStateException | UnsupportedOperationException e) { + LOG.debug("Unable to restrict permissions for {}: {}", f, e.getMessage()); + return false; + } + } + boolean ok = f.setReadable(false, false); + ok &= f.setWritable(false, false); + ok &= f.setExecutable(false, false); + ok &= f.setReadable(true, true); + if (ownerWritable) { + ok &= f.setWritable(true, true); + } + ok &= f.setExecutable(true, true); + if (!ok) { + LOG.debug( + "Unable to fully restrict permissions for {} on a file system without POSIX support", f); + } + return true; + } + + /** + * Removes any group/world permission bits from {@code f} while leaving the owner's own bits + * untouched, in a single atomic operation. Unlike {@link #restrictDirectoryToOwnerOnly(File)}, + * this never adds a permission (e.g. owner write) that {@code f} did not already have, so a path + * an operator deliberately made non-writable for the owner stays non-writable after repair. + * Returns {@code true} when the bits were applied or nothing needed stripping; on failure the + * caller must treat {@code f} as unusable. On Windows this is a no-op that returns {@code true}. + */ + static boolean stripGroupAndWorldBits(File f) { if (OperatingSystem.isWindows()) { - return; + return true; } try { - Path path = dir.toPath(); - Set perms = EnumSet.copyOf(Files.getPosixFilePermissions(path)); + Path path = f.toPath(); + Set perms = EnumSet.noneOf(PosixFilePermission.class); + perms.addAll(Files.getPosixFilePermissions(path)); perms.removeAll(GROUP_WORLD_BITS); Files.setPosixFilePermissions(path, perms); - } catch (IOException | IllegalStateException e) { - LOG.debug("Unable to strip group/world permissions for {}: {}", dir, e.getMessage()); + return true; + } catch (IOException | IllegalStateException | UnsupportedOperationException e) { + LOG.debug("Unable to strip group/world permissions for {}: {}", f, e.getMessage()); + return false; } } } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java index e4ae6fda673..a9e0753fed2 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/OOMENotifierScriptInitializer.java @@ -5,10 +5,10 @@ import static datadog.crashtracking.Initializer.findAgentJar; import static datadog.crashtracking.Initializer.getOomeNotifierTemplate; import static datadog.crashtracking.Initializer.getScriptPathFromArg; -import static datadog.crashtracking.Initializer.isOwnedAndPrivate; -import static datadog.crashtracking.Initializer.isSafeToRepairDirectory; +import static datadog.crashtracking.Initializer.isSafeToRepair; import static datadog.crashtracking.Initializer.pidFromSpecialFileName; import static datadog.crashtracking.Initializer.restrictDirectoryToOwnerOnly; +import static datadog.crashtracking.Initializer.restrictScriptToOwnerOnly; import static datadog.crashtracking.Initializer.stripGroupAndWorldBits; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; @@ -64,17 +64,23 @@ private static boolean copyOOMEscript(File scriptFile) { File scriptDirectory = scriptFile.getParentFile(); if (scriptDirectory.exists()) { - if (!isSafeToRepairDirectory(scriptDirectory)) { + if (!isSafeToRepair(scriptDirectory)) { LOG.warn( SEND_TELEMETRY, - "Untrusted OOME script folder {} (wrong owner or group/world bits set). OOME notification will not work properly.", + "Untrusted OOME script folder {} (wrong owner or group/world-writable). OOME notification will not work properly.", scriptDirectory); return false; } // owned by us but possibly left over from an older, less restrictive version: strip any // stray group/world bits without touching the owner's own bits, so a directory an operator // deliberately made non-writable stays non-writable - stripGroupAndWorldBits(scriptDirectory); + if (!stripGroupAndWorldBits(scriptDirectory)) { + LOG.warn( + SEND_TELEMETRY, + "Unable to strip group/world permissions from OOME script folder {}. OOME notification will not work properly.", + scriptDirectory); + return false; + } // cleanup all stale process-specific generated files in the parent folder of the given OOME // notifier script runScriptCleanup(scriptDirectory); @@ -93,35 +99,48 @@ private static boolean copyOOMEscript(File scriptFile) { scriptDirectory); return false; } - restrictDirectoryToOwnerOnly(scriptDirectory); + if (!restrictDirectoryToOwnerOnly(scriptDirectory)) { + LOG.warn( + SEND_TELEMETRY, + "Unable to restrict OOME script folder {} to owner-only permissions. OOME notification will not work properly.", + scriptDirectory); + return false; + } } try { // do not overwrite existing if (!scriptFile.exists()) { copyStream(getOomeNotifierTemplate(), scriptFile); - // first clear all privileges - scriptFile.setReadable(false, false); - scriptFile.setWritable(false, false); - scriptFile.setExecutable(false, false); - // then set them only for the owner - scriptFile.setReadable(true, true); - // do not restore the writable - scriptFile.setExecutable(true, true); + // fail closed: never leave a freshly written script we could not lock down + if (!restrictScriptToOwnerOnly(scriptFile)) { + scriptFile.delete(); + throw new IOException("Unable to restrict OOME script permissions"); + } } else { - if (!isOwnedAndPrivate(scriptFile)) { + // owned by us but possibly left over from an older, less restrictive version: repair in + // place by stripping stray group/world bits, preserving the owner's own bits + if (!isSafeToRepair(scriptFile)) { LOG.warn( SEND_TELEMETRY, "Untrusted OOME script {} (wrong owner or group/world-writable). OOME notification will not work properly.", scriptFile); return false; } + if (!stripGroupAndWorldBits(scriptFile)) { + LOG.warn( + SEND_TELEMETRY, + "Unable to strip group/world permissions from OOME script {}. OOME notification will not work properly.", + scriptFile); + return false; + } } } catch (IOException e) { LOG.warn( SEND_TELEMETRY, - "Failed to copy OOME script {}. OOME notification will not work properly.", - scriptFile); + "Failed to copy OOME script {} ({}). OOME notification will not work properly.", + scriptFile, + e.getMessage()); return false; } return true; diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java index 625e15031ea..c17d0416e08 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerSecurityTest.java @@ -69,6 +69,43 @@ void crashUploaderFreshDirIsOwnerRestricted() throws Exception { assertNoGroupWorldWriteBit(tempDir); } + @Test + void crashUploaderRepairsPreviouslyGeneratedScriptFile() throws Exception { + // Simulate a script file left behind by a pre-upgrade version of the initializer that did not + // clear group/world bits: owned by us and executable, but group/world readable (0555). + Path scriptFile = tempDir.resolve("dd_crash_uploader.sh"); + Files.createFile(scriptFile); + Files.setPosixFilePermissions(scriptFile, PosixFilePermissions.fromString("r-xr-xr-x")); + + assertTrue( + CrashUploaderScriptInitializer.initialize(scriptFile.toString(), "/tmp/hs_err.log"), + "Initializer should repair the stale script file instead of refusing it"); + + // Repair strips the group/world bits while preserving the owner's own bits + assertPermissions( + scriptFile, EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_EXECUTE)); + + // The repaired file must pass validation on the next JVM start as well + assertTrue(CrashUploaderScriptInitializer.initialize(scriptFile.toString(), "/tmp/hs_err.log")); + } + + @Test + void oomeNotifierRepairsPreviouslyGeneratedScriptFile() throws Exception { + Path scriptFile = tempDir.resolve("dd_oome_notifier.sh"); + Files.createFile(scriptFile); + Files.setPosixFilePermissions(scriptFile, PosixFilePermissions.fromString("r-xr-xr-x")); + + assertTrue( + OOMENotifierScriptInitializer.initialize(scriptFile + " %p"), + "Initializer should repair the stale script file instead of refusing it"); + + assertPermissions( + scriptFile, EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_EXECUTE)); + + // The repaired file must pass validation on the next JVM start as well + assertTrue(OOMENotifierScriptInitializer.initialize(scriptFile + " %p")); + } + @Test void crashUploaderHijackedScriptIsRefused() throws Exception { Path scriptFile = tempDir.resolve("dd_crash_uploader.sh"); @@ -190,9 +227,9 @@ void crashUploaderScriptFileHasNoGroupOrWorldReadBit() throws Exception { CrashUploaderScriptInitializer.initialize(scriptFile.toString(), "/tmp/hs_err.log"); // The script is created with FileOutputStream, so its initial mode is subject to the - // process umask (e.g. 0644 under a typical 0022 umask). The clear-then-set-owner-only - // sequence must strip any inherited group/other bits, not just overlay owner bits on top - // of them, otherwise a later JVM start rejects the script via isOwnedAndPrivate(). + // process umask (e.g. 0644 under a typical 0022 umask). The owner-only restriction must strip + // any inherited group/other bits, not just overlay owner bits on top of them, otherwise a + // later JVM start refuses the script as untrusted. assertPermissions( scriptFile, EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_EXECUTE)); }