//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the Containerization project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// #if os(Linux) import ArgumentParser import ContainerizationOS import LCShim #if canImport(Musl) import Musl private let _exit = Musl.exit private let _kill = Musl.kill #elseif canImport(Glibc) import Glibc private let _exit = Glibc.exit private let _kill = Glibc.kill #endif /// A minimal init process that: /// - Spawns and monitors a child process /// - Forwards signals to the child /// - Reaps zombie processes /// - Exits with the child's exit code public struct InitCommand: ParsableCommand { public static let configuration = CommandConfiguration( commandName: "init", abstract: "Run as a minimal init process" ) public init() {} @Flag(name: .shortAndLong, help: "Send signals to the child's process group instead of just the child") var processGroup: Bool = false @Argument(help: "The command to run") var command: String @Argument(parsing: .captureForPassthrough, help: "Arguments for the command") var arguments: [String] = [] /// Signals that should NOT be forwarded to the child. private static let ignoredSignals: Set = [ SIGCHLD, // We handle this for zombie reaping SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, // Synchronous signals ] public mutating func run() throws { // If we're not PID 1, register as a child subreaper so orphaned // processes get reparented to us and we can reap them. if getpid() != 1 { CZ_set_sub_reaper() } // Block all signals. We'll handle them synchronously via sigtimedwait var allSignals = sigset_t() sigfillset(&allSignals) sigprocmask(SIG_BLOCK, &allSignals, nil) let resolvedCommand = Path.lookPath(command)?.path ?? command var cmd = Command(resolvedCommand, arguments: arguments) cmd.stdin = .standardInput cmd.stdout = .standardOutput cmd.stderr = .standardError cmd.attrs = .init(setPGroup: true, setForegroundPGroup: true, setSignalDefault: true) try cmd.start() let childPid = cmd.pid let signalTarget = processGroup ? -childPid : childPid var timeout = timespec(tv_sec: 0, tv_nsec: 100_000_000) // Handle signals and reap zombies var childExitStatus: Int32? while childExitStatus == nil { var siginfo = siginfo_t() let sig = sigtimedwait(&allSignals, &siginfo, &timeout) if sig > 0 && !Self.ignoredSignals.contains(sig) { _ = _kill(signalTarget, sig) } while true { var status: Int32 = 0 let pid = waitpid(-1, &status, WNOHANG) if pid <= 0 { break } if pid == childPid { childExitStatus = Command.toExitStatus(status) } } } _exit(childExitStatus ?? 1) } } #endif