Merge nucleic/vivid-glass-urchin-xoym into main
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user