680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
166 lines
6.0 KiB
Swift
166 lines
6.0 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-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.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import ArgumentParser
|
|
import ContainerizationOCI
|
|
import ContainerizationOS
|
|
import FoundationEssentials
|
|
import LCShim
|
|
import Logging
|
|
import SystemPackage
|
|
|
|
#if canImport(Musl)
|
|
import Musl
|
|
#elseif canImport(Glibc)
|
|
import Glibc
|
|
#endif
|
|
|
|
struct ExecCommand: ParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "exec",
|
|
abstract: "Exec in a container"
|
|
)
|
|
|
|
@Option(name: .long, help: "path to an OCI runtime spec process configuration")
|
|
var processPath: String
|
|
|
|
@Option(name: .long, help: "pid of the init process for the container")
|
|
var parentPid: Int
|
|
|
|
func run() throws {
|
|
do {
|
|
let src = URL(fileURLWithPath: processPath)
|
|
let processBytes = try Data(contentsOf: src)
|
|
let process = try JSONDecoder().decode(
|
|
ContainerizationOCI.Process.self,
|
|
from: processBytes
|
|
)
|
|
try execInNamespaces(process: process)
|
|
} catch {
|
|
App.writeError(error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
static func enterNS(pidFd: Int32, nsType: Int32) throws {
|
|
guard setns(pidFd, nsType) == 0 else {
|
|
throw App.Errno(stage: "setns(fd)")
|
|
}
|
|
}
|
|
|
|
private func execInNamespaces(process: ContainerizationOCI.Process) throws {
|
|
let syncPipe = FileDescriptor(rawValue: 3)
|
|
let ackPipe = FileDescriptor(rawValue: 4)
|
|
|
|
let pidFd = CZ_pidfd_open(Int32(parentPid), 0)
|
|
guard pidFd > 0 else {
|
|
throw App.Errno(stage: "pidfd_open(\(parentPid))")
|
|
}
|
|
try Self.enterNS(
|
|
pidFd: pidFd,
|
|
nsType: CLONE_NEWCGROUP | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNS
|
|
)
|
|
|
|
let processID = fork()
|
|
|
|
guard processID != -1 else {
|
|
try? syncPipe.close()
|
|
try? ackPipe.close()
|
|
|
|
throw App.Errno(stage: "fork")
|
|
}
|
|
|
|
if processID == 0 { // child
|
|
// Wait for the grandparent to tell us that they acked our pid.
|
|
var pidAckBuffer = [UInt8](repeating: 0, count: App.ackPid.count)
|
|
let pidAckBytesRead = try pidAckBuffer.withUnsafeMutableBytes { buffer in
|
|
try ackPipe.read(into: buffer)
|
|
}
|
|
guard pidAckBytesRead > 0 else {
|
|
throw App.Failure(message: "read ack pipe")
|
|
}
|
|
let pidAckStr = String(decoding: pidAckBuffer[..<pidAckBytesRead], as: UTF8.self)
|
|
|
|
guard pidAckStr == App.ackPid else {
|
|
throw App.Failure(message: "received invalid acknowledgement string: \(pidAckStr)")
|
|
}
|
|
|
|
guard setsid() != -1 else {
|
|
throw App.Errno(stage: "setsid()")
|
|
}
|
|
|
|
if process.terminal {
|
|
let pty = try Console()
|
|
try pty.configureStdIO()
|
|
var masterFD = pty.master
|
|
|
|
try withUnsafeBytes(of: &masterFD) { bytes in
|
|
_ = try syncPipe.write(bytes)
|
|
}
|
|
|
|
// Wait for the grandparent to tell us that they acked our console.
|
|
var consoleAckBuffer = [UInt8](repeating: 0, count: App.ackConsole.count)
|
|
let consoleAckBytesRead = try consoleAckBuffer.withUnsafeMutableBytes { buffer in
|
|
try ackPipe.read(into: buffer)
|
|
}
|
|
guard consoleAckBytesRead > 0 else {
|
|
throw App.Failure(message: "read ack pipe")
|
|
}
|
|
let consoleAckStr = String(decoding: consoleAckBuffer[..<consoleAckBytesRead], as: UTF8.self)
|
|
|
|
guard consoleAckStr == App.ackConsole else {
|
|
throw App.Failure(message: "received invalid acknowledgement string: \(consoleAckStr)")
|
|
}
|
|
|
|
guard ioctl(0, UInt(TIOCSCTTY), 0) != -1 else {
|
|
throw App.Errno(stage: "setctty(0)")
|
|
}
|
|
try pty.close()
|
|
}
|
|
|
|
// Apply O_CLOEXEC to all file descriptors except stdio.
|
|
// This ensures that all unwanted fds we may have accidentally
|
|
// inherited are marked close-on-exec so they stay out of the
|
|
// container.
|
|
try App.applyCloseExecOnFDs()
|
|
try App.setRLimits(rlimits: process.rlimits)
|
|
|
|
// Prepare capabilities (before user change)
|
|
let preparedCaps = try App.prepareCapabilities(capabilities: process.capabilities ?? ContainerizationOCI.LinuxCapabilities())
|
|
|
|
// Change stdio to be owned by the requested user.
|
|
try App.fixStdioPerms(user: process.user)
|
|
|
|
// Set uid, gid, and supplementary groups
|
|
try App.setPermissions(user: process.user)
|
|
|
|
// Finish capabilities (after user change)
|
|
try App.finishCapabilities(preparedCaps)
|
|
|
|
// Set no_new_privs if requested by the OCI spec.
|
|
try App.setNoNewPrivileges(process: process)
|
|
|
|
try App.exec(process: process, currentEnv: process.env)
|
|
} else { // parent process
|
|
// Send our child's pid to our parent before we exit.
|
|
var childPid = processID
|
|
try withUnsafeBytes(of: &childPid) { bytes in
|
|
_ = try syncPipe.write(bytes)
|
|
}
|
|
}
|
|
}
|
|
}
|