Files
openclaw/apps/macos/Tests/OpenClawIPCTests/ExecSystemRunCommandValidatorTests.swift
Peter Steinberger 9b1c36d23c fix: prevent exec approval revocation races (#103515)
* fix(security): serialize exec approval mutations

* fix(security): preserve additive approval writes

* test(cli): expect normalized approval shape

* fix(security): preserve exec approval compatibility

* test(security): exercise locked approval initialization

* test(security): mock serialized approval helpers

* test(exec): derive enforced command path from plan

* fix(gateway): always return approval CAS conflicts

* fix(macos): serialize exec approvals writes

* fix(security): repair approval build errors

* fix(security): serialize exec approval mutations

* fix(security): fail closed on approval persistence errors

* test(security): cover detached approval persistence failures

* fix(security): harden exec approval state

* style(macos): format exec approval sources

* fix(security): complete exec approval hardening

Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>

* fix(macos): preserve approved login-shell semantics

* fix(macos): keep login shell approvals one-shot

* fix(security): linearize exec authorization

Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>

* fix(security): preserve durable approval basis

Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>

* fix(security): bind exec grants to current policy

Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>

* test(security): fix exec revocation fixtures

* test(security): align gateway approval fixtures

* fix(macos): return approval decisions

* chore(i18n): sync native approval strings

* test(security): align approval hardening fixtures

* test(node): authorize completed event fixture

* test(security): fix approval decision fixtures

* test(security): await durable approval visibility

* fix(exec): preserve concurrent approval grants

* fix(exec): address exact-head CI failures

* fix(exec): preserve concurrent approval promotions

* fix(exec): make Swift shutdown state explicit

* test(macos): handle approval read failures

* fix(macos): harden approval socket paths

* fix(macos): preserve exact shell payload bytes

* test(macos): make approval fixtures explicit

* test(macos): fix approval suite compilation

* fix(macos): bound approval socket JSONL reads

* chore: move exec approval note to release process

* chore: move exec approval note to release process

---------

Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
2026-07-10 21:35:05 +01:00

180 lines
7.4 KiB
Swift

import Foundation
import Testing
@testable import OpenClaw
private struct SystemRunCommandContractFixture: Decodable {
let cases: [SystemRunCommandContractCase]
}
private struct SystemRunCommandContractCase: Decodable {
let name: String
let command: [String]
let rawCommand: String?
let expected: SystemRunCommandContractExpected
}
private struct SystemRunCommandContractExpected: Decodable {
let valid: Bool
let displayCommand: String?
let errorContains: String?
}
struct ExecSystemRunCommandValidatorTests {
@Test func `matches shared system run command contract fixture`() throws {
for entry in try Self.loadContractCases() {
let result = ExecSystemRunCommandValidator.resolve(command: entry.command, rawCommand: entry.rawCommand)
if !entry.expected.valid {
switch result {
case let .ok(resolved):
Issue
.record("\(entry.name): expected invalid result, got displayCommand=\(resolved.displayCommand)")
case let .invalid(message):
if let expected = entry.expected.errorContains {
#expect(
message.contains(expected),
"\(entry.name): expected error containing \(expected), got \(message)")
}
}
continue
}
switch result {
case let .ok(resolved):
#expect(
resolved.displayCommand == entry.expected.displayCommand,
"\(entry.name): unexpected display command")
case let .invalid(message):
Issue.record("\(entry.name): unexpected invalid result: \(message)")
}
}
}
@Test func `validator keeps canonical wrapper text out of allowlist raw parsing`() {
let command = ["/bin/sh", "-lc", "/usr/bin/printf ok"]
let rawCommand = "/bin/sh -lc \"/usr/bin/printf ok\""
let result = ExecSystemRunCommandValidator.resolve(command: command, rawCommand: rawCommand)
switch result {
case let .ok(resolved):
#expect(resolved.displayCommand == rawCommand)
#expect(resolved.evaluationRawCommand == nil)
case let .invalid(message):
Issue.record("unexpected invalid result: \(message)")
}
}
@Test func `canonical display preserves padded argv bytes`() {
let command = ["/usr/bin/printf", "<%s>", " padded "]
let canonical = "/usr/bin/printf \"<%s>\" \" padded \""
switch ExecSystemRunCommandValidator.resolve(command: command, rawCommand: canonical) {
case let .ok(resolved):
#expect(resolved.displayCommand == canonical)
case let .invalid(message):
Issue.record("unexpected invalid result: \(message)")
}
}
@Test func `canonical display makes metacharacters and escapes unambiguous`() {
let command = ["printf", "<%s>", #"a\"b"#, "$(id)", "x;y", "a\nb"]
let canonical = #"printf "<%s>" "a\\\"b" "$(id)" "x;y" "a\nb""#
#expect(ExecCommandFormatter.displayString(for: command) == canonical)
switch ExecSystemRunCommandValidator.resolve(command: ["printf", "<%s>"], rawCommand: "printf <%s>") {
case let .ok(resolved):
#expect(resolved.displayCommand == #"printf "<%s>""#)
case let .invalid(message):
Issue.record("legacy display spelling was rejected: \(message)")
}
}
@Test func `env dash shell wrapper requires canonical raw command binding`() {
let command = ["/usr/bin/env", "-", "bash", "-lc", "echo hi"]
let legacy = ExecSystemRunCommandValidator.resolve(command: command, rawCommand: "echo hi")
switch legacy {
case .ok:
Issue.record("expected rawCommand mismatch for env dash prelude")
case let .invalid(message):
#expect(message.contains("rawCommand does not match command"))
}
let canonicalRaw = "/usr/bin/env - bash -lc \"echo hi\""
let canonical = ExecSystemRunCommandValidator.resolve(command: command, rawCommand: canonicalRaw)
switch canonical {
case let .ok(resolved):
#expect(resolved.displayCommand == canonicalRaw)
case let .invalid(message):
Issue.record("unexpected invalid result for canonical raw command: \(message)")
}
}
@Test func `fish attached c command requires canonical raw command binding`() {
let command = ["/usr/bin/fish", "-c/tmp/payload.fish", "/usr/bin/printf safe_marker"]
let result = ExecSystemRunCommandValidator.resolve(
command: command,
rawCommand: "/usr/bin/printf safe_marker")
switch result {
case .ok:
Issue.record("expected rawCommand mismatch for attached fish command payload")
case let .invalid(message):
#expect(message.contains("rawCommand does not match command"))
}
}
@Test func `startup shell wrappers require canonical raw command binding`() {
for command in [
["/bin/bash", "-lc", "/usr/bin/printf safe_marker"],
["/bin/bash", "--rcfile", "/tmp/payload.sh", "-i", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "--login", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "--init-command=/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
] {
let legacy = ExecSystemRunCommandValidator.resolve(
command: command,
rawCommand: "/usr/bin/printf safe_marker")
switch legacy {
case .ok:
Issue.record("expected rawCommand mismatch for startup shell wrapper")
case let .invalid(message):
#expect(message.contains("rawCommand does not match command"))
}
let canonicalRaw = ExecCommandFormatter.displayString(for: command)
let canonical = ExecSystemRunCommandValidator.resolve(command: command, rawCommand: canonicalRaw)
switch canonical {
case let .ok(resolved):
#expect(resolved.displayCommand == canonicalRaw)
case let .invalid(message):
Issue.record("unexpected invalid result for canonical raw command: \(message)")
}
}
}
private static func loadContractCases() throws -> [SystemRunCommandContractCase] {
let fixtureURL = try self.findContractFixtureURL()
let data = try Data(contentsOf: fixtureURL)
let decoded = try JSONDecoder().decode(SystemRunCommandContractFixture.self, from: data)
return decoded.cases
}
private static func findContractFixtureURL() throws -> URL {
var cursor = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
for _ in 0..<8 {
let candidate = cursor
.appendingPathComponent("test")
.appendingPathComponent("fixtures")
.appendingPathComponent("system-run-command-contract.json")
if FileManager.default.fileExists(atPath: candidate.path) {
return candidate
}
cursor.deleteLastPathComponent()
}
throw NSError(
domain: "ExecSystemRunCommandValidatorTests",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "missing shared system-run command contract fixture"])
}
}