Merge nucleic/vivid-glass-urchin-xoym into main

This commit is contained in:
2026-08-08 17:19:44 -07:00
parent 26739f9487
commit de3fc45777
+56 -21
View File
@@ -50,6 +50,8 @@ public enum LocalNetworkPermission {
case probeProducedNoReport
/// A subnet argument is not an IPv4 address or CIDR block.
case invalidSubnet(String)
/// A child process could not be started or waited on.
case spawnFailed(command: String, code: Int32)
public var description: String {
switch self {
@@ -74,6 +76,8 @@ public enum LocalNetworkPermission {
entries it cannot parse, which would leave the allowlist looking configured \
while granting nothing.
"""
case .spawnFailed(let command, let code):
return "could not run \(command): \(String(cString: strerror(code))) (\(code))"
}
}
}
@@ -117,27 +121,15 @@ public enum LocalNetworkPermission {
let commands = LocalNetworkPolicy.writeCommandLines(subnets: subnets)
for arguments in LocalNetworkPolicy.writeArguments(subnets: subnets) {
for (index, arguments) in LocalNetworkPolicy.writeArguments(subnets: subnets).enumerated() {
let sudoArguments = (allowPasswordPrompt ? [] : ["-n"]) + ["/usr/bin/defaults"] + arguments
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
process.arguments = sudoArguments
// Stdio is deliberately inherited rather than piped. sudo reads the
// password from /dev/tty and would work either way, but its prompt
// and any "not in the sudoers file" complaint belong in front of
// the operator, not captured and paraphrased.
try process.run()
process.waitUntilExit()
guard process.terminationStatus == 0 else {
let index = LocalNetworkPolicy.writeArguments(subnets: subnets)
.firstIndex(of: arguments) ?? 0
let exitCode = try runInForeground("/usr/bin/sudo", sudoArguments)
guard exitCode == 0 else {
if !allowPasswordPrompt {
throw PermissionError.needsPassword(commands: commands)
}
throw PermissionError.writeFailed(
command: commands[index], exitCode: process.terminationStatus)
throw PermissionError.writeFailed(command: commands[index], exitCode: exitCode)
}
}
@@ -147,11 +139,54 @@ public enum LocalNetworkPermission {
/// Reboots the host. Only ever called from an explicit confirmation — the
/// allowlist is read at boot, so nothing else makes it take effect.
public static func reboot() throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
process.arguments = ["/sbin/shutdown", "-r", "now"]
try process.run()
process.waitUntilExit()
_ = try runInForeground("/usr/bin/sudo", ["/sbin/shutdown", "-r", "now"])
}
/// Runs a command with this process's stdio *and its process group*, and
/// returns its exit status.
///
/// The process group is the whole reason this is not `Foundation.Process`.
/// `Process` starts the child as its own process-group leader, so for the
/// controlling terminal the child is a *background* job — and the terminal
/// driver defends itself against those. `sudo`'s `tcsetattr` to turn echo
/// off raises `SIGTTOU` and fails, so the password is typed in the clear;
/// its read of the tty raises `SIGTTIN`, so Return never reaches `sudo` and
/// the line editor just echoes a newline. Both symptoms, one cause.
///
/// `posix_spawn` with no `POSIX_SPAWN_SETPGROUP` leaves the child in our
/// process group, which is the terminal's foreground group, so `sudo` gets
/// the terminal it expects. Stdio is inherited for the same reason it
/// always was: the prompt and any "not in the sudoers file" complaint
/// belong in front of the operator, not captured and paraphrased.
private static func runInForeground(_ executable: String, _ arguments: [String]) throws -> Int32
{
var argv: [UnsafeMutablePointer<CChar>?] = ([executable] + arguments).map { strdup($0) }
argv.append(nil)
var envp: [UnsafeMutablePointer<CChar>?] = ProcessInfo.processInfo.environment.map {
strdup("\($0.key)=\($0.value)")
}
envp.append(nil)
defer {
for pointer in argv { free(pointer) }
for pointer in envp { free(pointer) }
}
var pid: pid_t = 0
let spawned = posix_spawn(&pid, executable, nil, nil, argv, envp)
guard spawned == 0 else {
throw PermissionError.spawnFailed(command: executable, code: spawned)
}
var status: Int32 = 0
while waitpid(pid, &status, 0) < 0 {
guard errno == EINTR else {
throw PermissionError.spawnFailed(command: executable, code: errno)
}
}
// WIFEXITED and friends are C macros, so Swift does not import them.
let terminatingSignal = status & 0x7F
return terminatingSignal == 0 ? (status >> 8) & 0xFF : 128 + terminatingSignal
}
// MARK: - Interactive: the system prompt