fix(macos): address remaining exec review comments

This commit is contained in:
Nimrod Gutman 2026-03-19 14:53:44 +02:00
parent f55e51afb5
commit 939481e22f
3 changed files with 28 additions and 16 deletions

View File

@ -44,13 +44,12 @@ struct ExecCommandResolution {
return []
}
var resolutions: [ExecCommandResolution] = []
resolutions.reserveCapacity(segments.count)
for segment in segments {
guard let resolution = self.resolveShellSegmentExecutable(segment, cwd: cwd, env: env)
else {
let segmentResolutions = self.resolveShellSegmentExecutions(segment, cwd: cwd, env: env)
guard !segmentResolutions.isEmpty else {
return []
}
resolutions.append(resolution)
resolutions.append(contentsOf: segmentResolutions)
}
return resolutions
}
@ -113,18 +112,14 @@ struct ExecCommandResolution {
cwd: cwd)
}
private static func resolveShellSegmentExecutable(
private static func resolveShellSegmentExecutions(
_ segment: String,
cwd: String?,
env: [String: String]?) -> ExecCommandResolution?
env: [String: String]?) -> [ExecCommandResolution]
{
let tokens = self.tokenizeShellWords(segment)
guard !tokens.isEmpty else { return nil }
let effective = ExecWrapperResolution.unwrapDispatchWrappersForResolution(tokens)
guard let raw = effective.first?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
return nil
}
return self.resolveExecutable(rawExecutable: raw, cwd: cwd, env: env)
guard !tokens.isEmpty else { return [] }
return self.resolveForAllowlist(command: tokens, rawCommand: nil, cwd: cwd, env: env)
}
private static func collectAllowAlwaysPatterns(
@ -139,6 +134,11 @@ struct ExecCommandResolution {
return
}
// Allow-always persistence intentionally peels known dispatch wrappers
// directly so approvals stay scoped to the launched executable instead of
// the wrapper binary. The allowlist path stays stricter for semantic
// wrapper usage (for example `env FOO=bar ...`) and may still require
// re-approval in those cases.
switch ExecWrapperResolution.unwrapKnownDispatchWrapperInvocation(command) {
case .blocked:
return

View File

@ -88,8 +88,4 @@ enum ExecEnvInvocationUnwrapper {
guard !expectsOptionValue, idx < command.count else { return nil }
return UnwrapResult(command: Array(command[idx...]), usesModifiers: usesModifiers)
}
static func unwrapDispatchWrappersForResolution(_ command: [String]) -> [String] {
ExecWrapperResolution.unwrapDispatchWrappersForResolution(command)
}
}

View File

@ -264,6 +264,22 @@ struct ExecAllowlistTests {
#expect(resolutions[1].executableName == "whoami")
}
@Test func `resolve for allowlist recurses through nested shell wrappers after dispatch wrappers`() throws {
let tmp = try makeTempDirForTests()
let whoami = tmp.appendingPathComponent("whoami")
try makeExecutableForTests(at: whoami)
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: ["/bin/sh", "-lc", "nice /bin/zsh -lc whoami"],
rawCommand: nil,
cwd: tmp.path,
env: ["PATH": "\(tmp.path):/usr/bin:/bin"])
#expect(resolutions.count == 1)
#expect(resolutions[0].resolvedPath == whoami.path)
#expect(resolutions[0].executableName == "whoami")
}
@Test func `resolve for allowlist unwraps direct dispatch wrappers with canonical raw command`() {
let command = ["/usr/bin/nice", "/usr/bin/printf", "ok"]
let resolutions = ExecCommandResolution.resolveForAllowlist(