diff --git a/Sources/TerminalNotificationPolicy.swift b/Sources/TerminalNotificationPolicy.swift index 3a5e8a606e14..d811ea4c5852 100644 --- a/Sources/TerminalNotificationPolicy.swift +++ b/Sources/TerminalNotificationPolicy.swift @@ -619,7 +619,9 @@ private final class NotificationHookProcessRun: @unchecked Sendable { posix_spawnattr_setsigmask(&attributes, &emptyMask), operation: "clear inherited signal mask" ) - let flags = Int16(POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGMASK) + // Keep unrelated app descriptors out of hooks. The dup2 actions above + // preserve the hook's standard streams. + let flags = Int16(POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_CLOEXEC_DEFAULT) try throwIfPOSIXError(posix_spawnattr_setflags(&attributes, flags), operation: "set spawn flags") try throwIfPOSIXError(posix_spawnattr_setpgroup(&attributes, 0), operation: "set process group") let arguments = ["/bin/sh", "-c", hook.command] diff --git a/cmuxTests/NotificationAndMenuBarTests.swift b/cmuxTests/NotificationAndMenuBarTests.swift index 8408191df7c3..d429875c9703 100644 --- a/cmuxTests/NotificationAndMenuBarTests.swift +++ b/cmuxTests/NotificationAndMenuBarTests.swift @@ -1,6 +1,8 @@ import XCTest import AppKit +import Darwin import SwiftUI +import Testing import UniformTypeIdentifiers import WebKit import ObjectiveC.runtime @@ -35,6 +37,47 @@ private final class NotificationHookEvaluationResultBox: @unchecked Sendable { } } +@Suite("Notification hook process isolation") +struct NotificationHookProcessIsolationTests { + @Test + func hookDoesNotInheritUnrelatedParentFileDescriptor() async throws { + var unrelatedPipe = [Int32](repeating: -1, count: 2) + try #require(Darwin.pipe(&unrelatedPipe) == 0) + defer { + for descriptor in unrelatedPipe where descriptor >= 0 { + Darwin.close(descriptor) + } + } + let unrelatedDescriptor = try #require(unrelatedPipe.first) + try #require(unrelatedDescriptor > STDERR_FILENO) + + let request = TerminalNotificationPolicyRequest( + tabId: UUID(), + surfaceId: nil, + title: "Title", + subtitle: "", + body: "Body", + cwd: FileManager.default.temporaryDirectory.path, + isAppFocused: false, + isFocusedPanel: false + ) + let hook = CmuxResolvedNotificationHook( + id: "descriptor-isolation", + command: "if [ -e /dev/fd/\(unrelatedDescriptor) ]; then printf '{\"notification\":{\"body\":\"inherited\"}}'; else cat; fi", + timeoutSeconds: 5, + sourcePath: "/tmp/cmux.json", + cwd: FileManager.default.temporaryDirectory.path + ) + + let result = await TerminalNotificationPolicyEngine.evaluate( + request: request, + hooks: [hook] + ) + let envelope = try result.get() + #expect(envelope.notification.body == "Body") + } +} + final class TerminalNotificationPolicyEngineTests: XCTestCase { private func evaluate( request: TerminalNotificationPolicyRequest,