From 26b033d51fd627af0cfdf47efcbdb346484e1c85 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:48:22 +0800 Subject: [PATCH] fix(process): classify withheld Darwin environments before parser failures --- bridge/process/ProcessRunTokenReader.swift | 45 ++++++++++--------- .../darwinProcessRunTokenReader.test.ts | 23 ++++++++++ .../fixtures/processRunTokenParserProbe.swift | 37 +++++++++++++++ 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/bridge/process/ProcessRunTokenReader.swift b/bridge/process/ProcessRunTokenReader.swift index 599fbdb1b4..931eb48750 100644 --- a/bridge/process/ProcessRunTokenReader.swift +++ b/bridge/process/ProcessRunTokenReader.swift @@ -228,6 +228,22 @@ func processRunTokenObservation( parse: Result, codeSigningFlags: () -> UInt32? ) -> RunTokenObservation { + // Native inspection restrictions apply even when argv is incomplete or + // overwritten, before parsing reaches the environment. Keep this fact at + // the native boundary; cleanup still decides whether the process is related. + func withheldEnvironment() -> RunTokenObservation? { + guard let flags = codeSigningFlags() else { return nil } + if (flags & csPlatformBinary) != 0 { + return unavailable(pid: pid, reason: "platform_binary_environment_withheld") + } + if (flags & csRestrict) != 0 { + return unavailable(pid: pid, reason: "restricted_process_environment_withheld") + } + return nil + } + func unavailableEnvironment(_ reason: String) -> RunTokenObservation { + withheldEnvironment() ?? unavailable(pid: pid, reason: reason) + } switch parse { case .success(let parsed): if let runId = parsed.runId { @@ -238,40 +254,25 @@ func processRunTokenObservation( if parsed.environmentEntryCount > 0 { return RunTokenObservation(pid: pid, state: "readable", run_id: nil, reason: nil) } - let flags = codeSigningFlags() - if let flags, (flags & csPlatformBinary) != 0 { - return unavailable(pid: pid, reason: "platform_binary_environment_withheld") - } - if let flags, (flags & csRestrict) != 0 { - return unavailable(pid: pid, reason: "restricted_process_environment_withheld") - } + if let withheld = withheldEnvironment() { return withheld } return RunTokenObservation(pid: pid, state: "readable", run_id: nil, reason: nil) case .failure(let failure): switch failure { case .environmentUnavailable: - let flags = codeSigningFlags() - if let flags, (flags & csPlatformBinary) != 0 { - return unavailable(pid: pid, reason: "platform_binary_environment_withheld") - } - if let flags, (flags & csRestrict) != 0 { - return unavailable(pid: pid, reason: "restricted_process_environment_withheld") - } - return unavailable(pid: pid, reason: "environment_unavailable") + return unavailableEnvironment("environment_unavailable") case .ambiguousEnvironmentBoundary: - if let flags = codeSigningFlags(), (flags & csRestrict) != 0 { - return unavailable(pid: pid, reason: "restricted_process_environment_withheld") - } - return unavailable(pid: pid, reason: "ambiguous_environment_boundary") + return unavailableEnvironment("ambiguous_environment_boundary") + // These failures already establish that token bytes were available. case .duplicateToken: return unavailable(pid: pid, reason: "duplicate_run_token") case .invalidTokenEncoding: return unavailable(pid: pid, reason: "invalid_run_token_encoding") case .appleVectorUnavailable: - return unavailable(pid: pid, reason: "apple_vector_unavailable") + return unavailableEnvironment("apple_vector_unavailable") case .invalidBuffer: - return unavailable(pid: pid, reason: "malformed_procargs") + return unavailableEnvironment("malformed_procargs") case .invalidBufferAt(let stage): - return unavailable(pid: pid, reason: "malformed_procargs_\(stage.rawValue)") + return unavailableEnvironment("malformed_procargs_\(stage.rawValue)") } } } diff --git a/tests/boundary/process/darwinProcessRunTokenReader.test.ts b/tests/boundary/process/darwinProcessRunTokenReader.test.ts index 247d3e3639..63f77789c4 100644 --- a/tests/boundary/process/darwinProcessRunTokenReader.test.ts +++ b/tests/boundary/process/darwinProcessRunTokenReader.test.ts @@ -1317,6 +1317,29 @@ it.skipIf(!onDarwin)( missingAppleBoundaryFailsClosed: true, malformedBuffersRejected: true, invalidPointerSizeFailsClosed: true, + incompleteRestrictedArguments: [ + "malformed_procargs_path_padding", + "malformed_procargs_arguments", + "apple_vector_unavailable", + ].map((reason) => [ + { + state: "unavailable", + reason: "platform_binary_environment_withheld", + }, + { + state: "unavailable", + reason: "restricted_process_environment_withheld", + }, + { state: "unavailable", reason }, + ]), + signedReadableTokens: [true, true], + signedInvalidTokens: [ + "duplicate_run_token", + "invalid_run_token_encoding", + ].map((reason) => [ + { state: "unavailable", reason }, + { state: "unavailable", reason }, + ]), malformedStages: [ { state: "unavailable", reason: "malformed_procargs_header" }, { state: "unavailable", reason: "malformed_procargs_argument_count" }, diff --git a/tests/fixtures/processRunTokenParserProbe.swift b/tests/fixtures/processRunTokenParserProbe.swift index ed359869d8..335ce435b3 100644 --- a/tests/fixtures/processRunTokenParserProbe.swift +++ b/tests/fixtures/processRunTokenParserProbe.swift @@ -193,6 +193,40 @@ struct ProcessRunTokenParserProbe { parse: .failure(.invalidBuffer), codeSigningFlags: { 0 } ) + // A restricted native process can expose an incomplete argv layout before + // the parser reaches the withheld environment (CI observed mdworker here). + let incompleteRestrictedArguments = [ + RunTokenReadError.invalidBufferAt(.pathPadding), + .invalidBufferAt(.arguments), + .appleVectorUnavailable, + ].map { failure in + [UInt32(0x0400_0000), UInt32(0x0000_0800), nil].map { flags in + let observation = processRunTokenObservation( + pid: 1, + parse: .failure(failure), + codeSigningFlags: { flags } + ) + return ["state": observation.state, "reason": observation.reason ?? ""] + } + } + let signedReadableTokens = [UInt32(0x0400_0000), UInt32(0x0000_0800)].map { flags in + let observation = processRunTokenObservation( + pid: 1, + parse: .success(realToken), + codeSigningFlags: { flags } + ) + return observation.state == "readable" && observation.run_id == "owned-token" + } + let signedInvalidTokens = [RunTokenReadError.duplicateToken, .invalidTokenEncoding].map { failure in + [UInt32(0x0400_0000), UInt32(0x0000_0800)].map { flags in + let observation = processRunTokenObservation( + pid: 1, + parse: .failure(failure), + codeSigningFlags: { flags } + ) + return ["state": observation.state, "reason": observation.reason ?? ""] + } + } let duplicateTokenFailsClosed: Bool do { _ = try parseProcessEnvironment(in: fixture( @@ -311,6 +345,9 @@ struct ProcessRunTokenParserProbe { "malformedBuffersRejected": malformedBuffersRejected, "invalidPointerSizeFailsClosed": invalidPointerSizeFailsClosed, "malformedStages": malformedStages, + "incompleteRestrictedArguments": incompleteRestrictedArguments, + "signedReadableTokens": signedReadableTokens, + "signedInvalidTokens": signedInvalidTokens, "malformedObservation": [ "state": malformedObservation.state, "reason": malformedObservation.reason as Any? ?? NSNull(),