Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Sources/TerminalNotificationPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
43 changes: 43 additions & 0 deletions cmuxTests/NotificationAndMenuBarTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import XCTest
import AppKit
import Darwin
import SwiftUI
import Testing
import UniformTypeIdentifiers
import WebKit
import ObjectiveC.runtime
Expand Down Expand Up @@ -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,
Expand Down
Loading