mirror of
https://github.com/apple/container.git
synced 2026-09-12 10:45:42 +00:00
LinuxPod: Wire up pid namespace sharing (#434)
In a prior change I'd added a way for vminitd to double as a simple pause container. This change wires this up by adding a new bool to the pod config to ask for pid ns sharing.
This commit is contained in:
@@ -50,6 +50,9 @@ public final class LinuxPod: Sendable {
|
||||
public var virtualization: Bool = false
|
||||
/// Optional file path to store serial boot logs.
|
||||
public var bootLog: BootLog?
|
||||
/// Whether containers in the pod should share a PID namespace.
|
||||
/// When enabled, all containers can see each other's processes.
|
||||
public var shareProcessNamespace: Bool = false
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -103,6 +106,7 @@ public final class LinuxPod: Sendable {
|
||||
private struct State: Sendable {
|
||||
var phase: Phase
|
||||
var containers: [String: PodContainer]
|
||||
var pauseProcess: LinuxProcess?
|
||||
}
|
||||
|
||||
private enum Phase: Sendable {
|
||||
@@ -173,7 +177,7 @@ public final class LinuxPod: Sendable {
|
||||
try configuration(&config)
|
||||
|
||||
self.config = config
|
||||
self.state = AsyncMutex(State(phase: .initialized, containers: [:]))
|
||||
self.state = AsyncMutex(State(phase: .initialized, containers: [:], pauseProcess: nil))
|
||||
}
|
||||
|
||||
private static func createDefaultRuntimeSpec(_ containerID: String, podID: String) -> Spec {
|
||||
@@ -303,9 +307,64 @@ extension LinuxPod {
|
||||
|
||||
do {
|
||||
let containers = state.containers
|
||||
let shareProcessNamespace = self.config.shareProcessNamespace
|
||||
let pauseProcessHolder = Mutex<LinuxProcess?>(nil)
|
||||
|
||||
try await vm.withAgent { agent in
|
||||
try await agent.standardSetup()
|
||||
|
||||
// Create pause container if PID namespace sharing is enabled
|
||||
if shareProcessNamespace {
|
||||
let pauseID = "pause-\(self.id)"
|
||||
let pauseRootfsPath = "/run/container/\(pauseID)/rootfs"
|
||||
|
||||
// Bind mount /sbin into the pause container rootfs.
|
||||
// This is where the guest agent lives.
|
||||
try await agent.mount(
|
||||
ContainerizationOCI.Mount(
|
||||
type: "",
|
||||
source: "/sbin",
|
||||
destination: "\(pauseRootfsPath)/sbin",
|
||||
options: ["bind"]
|
||||
))
|
||||
|
||||
var pauseSpec = Self.createDefaultRuntimeSpec(pauseID, podID: self.id)
|
||||
pauseSpec.process?.args = ["/sbin/vminitd", "pause"]
|
||||
pauseSpec.hostname = ""
|
||||
pauseSpec.mounts = LinuxContainer.defaultMounts().map {
|
||||
ContainerizationOCI.Mount(
|
||||
type: $0.type,
|
||||
source: $0.source,
|
||||
destination: $0.destination,
|
||||
options: $0.options
|
||||
)
|
||||
}
|
||||
pauseSpec.linux?.namespaces = [
|
||||
LinuxNamespace(type: .cgroup),
|
||||
LinuxNamespace(type: .ipc),
|
||||
LinuxNamespace(type: .mount),
|
||||
LinuxNamespace(type: .pid),
|
||||
LinuxNamespace(type: .uts),
|
||||
]
|
||||
|
||||
// Create LinuxProcess for pause container
|
||||
let process = LinuxProcess(
|
||||
pauseID,
|
||||
containerID: pauseID,
|
||||
spec: pauseSpec,
|
||||
io: LinuxProcess.Stdio(stdin: nil, stdout: nil, stderr: nil),
|
||||
ociRuntimePath: nil,
|
||||
agent: agent,
|
||||
vm: vm,
|
||||
logger: self.logger
|
||||
)
|
||||
|
||||
try await process.start()
|
||||
pauseProcessHolder.withLock { $0 = process }
|
||||
|
||||
self.logger?.debug("Pause container started", metadata: ["pid": "\(process.pid)"])
|
||||
}
|
||||
|
||||
// Mount all container rootfs
|
||||
for (_, container) in containers {
|
||||
guard let attachments = vm.mounts[container.id], let rootfsAttachment = attachments.first else {
|
||||
@@ -353,6 +412,8 @@ extension LinuxPod {
|
||||
}
|
||||
}
|
||||
|
||||
state.pauseProcess = pauseProcessHolder.withLock { $0 }
|
||||
|
||||
// Transition all containers to created state
|
||||
for id in state.containers.keys {
|
||||
state.containers[id]?.state = .created
|
||||
@@ -394,6 +455,33 @@ extension LinuxPod {
|
||||
let containerMounts = createdState.vm.mounts[containerID] ?? []
|
||||
spec.mounts = containerMounts.dropFirst().map { $0.to }
|
||||
|
||||
// Configure namespaces for the container
|
||||
var namespaces: [LinuxNamespace] = [
|
||||
LinuxNamespace(type: .cgroup),
|
||||
LinuxNamespace(type: .ipc),
|
||||
LinuxNamespace(type: .mount),
|
||||
LinuxNamespace(type: .uts),
|
||||
]
|
||||
|
||||
// Either join pause container's pid ns or create a new one
|
||||
if self.config.shareProcessNamespace, let pausePID = state.pauseProcess?.pid {
|
||||
let nsPath = "/proc/\(pausePID)/ns/pid"
|
||||
|
||||
self.logger?.debug(
|
||||
"Container joining pause PID namespace",
|
||||
metadata: [
|
||||
"container": "\(containerID)",
|
||||
"pausePID": "\(pausePID)",
|
||||
"nsPath": "\(nsPath)",
|
||||
])
|
||||
|
||||
namespaces.append(LinuxNamespace(type: .pid, path: nsPath))
|
||||
} else {
|
||||
namespaces.append(LinuxNamespace(type: .pid))
|
||||
}
|
||||
|
||||
spec.linux?.namespaces = namespaces
|
||||
|
||||
let stdio = IOUtil.setup(
|
||||
portAllocator: self.hostVsockPorts,
|
||||
stdin: container.config.process.stdin,
|
||||
|
||||
@@ -702,4 +702,48 @@ extension IntegrationSuite {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func testPodSharedPIDNamespace() async throws {
|
||||
let id = "test-pod-shared-pid-namespace"
|
||||
|
||||
let bs = try await bootstrap(id)
|
||||
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
||||
config.cpus = 4
|
||||
config.memoryInBytes = 1024.mib()
|
||||
config.bootLog = bs.bootLog
|
||||
config.shareProcessNamespace = true
|
||||
}
|
||||
|
||||
// First container runs a long-running process
|
||||
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
|
||||
config.process.arguments = ["/bin/sleep", "300"]
|
||||
}
|
||||
|
||||
// Second container checks if it can see container1's sleep process
|
||||
let psBuffer = BufferWriter()
|
||||
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
|
||||
config.process.arguments = ["/bin/sh", "-c", "ps aux | grep 'sleep 300' | grep -v grep"]
|
||||
config.process.stdout = psBuffer
|
||||
}
|
||||
|
||||
try await pod.create()
|
||||
try await pod.startContainer("container1")
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
try await pod.startContainer("container2")
|
||||
let status = try await pod.waitContainer("container2")
|
||||
|
||||
try await pod.killContainer("container1", signal: SIGKILL)
|
||||
_ = try await pod.waitContainer("container1")
|
||||
try await pod.stop()
|
||||
|
||||
guard status.exitCode == 0 else {
|
||||
throw IntegrationError.assert(msg: "container2 should have found the sleep process (status: \(status))")
|
||||
}
|
||||
|
||||
let output = String(data: psBuffer.data, encoding: .utf8) ?? ""
|
||||
guard output.contains("sleep 300") else {
|
||||
throw IntegrationError.assert(msg: "ps output should contain 'sleep 300', got: '\(output)'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,7 @@ struct IntegrationSuite: AsyncParsableCommand {
|
||||
Test("pod container filesystem isolation", testPodContainerFilesystemIsolation),
|
||||
Test("pod container PID namespace isolation", testPodContainerPIDNamespaceIsolation),
|
||||
Test("pod container independent resource limits", testPodContainerIndependentResourceLimits),
|
||||
Test("pod shared PID namespace", testPodSharedPIDNamespace),
|
||||
]
|
||||
|
||||
let passed: Atomic<Int> = Atomic(0)
|
||||
|
||||
@@ -36,9 +36,14 @@ struct RunCommand: ParsableCommand {
|
||||
LoggingSystem.bootstrap(App.standardError)
|
||||
let log = Logger(label: "vmexec")
|
||||
|
||||
let bundle = try ContainerizationOCI.Bundle.load(path: URL(filePath: bundlePath))
|
||||
let ociSpec = try bundle.loadConfig()
|
||||
try execInNamespace(spec: ociSpec, log: log)
|
||||
let spec: ContainerizationOCI.Spec
|
||||
do {
|
||||
let bundle = try ContainerizationOCI.Bundle.load(path: URL(filePath: bundlePath))
|
||||
spec = try bundle.loadConfig()
|
||||
} catch {
|
||||
throw App.Failure(message: "failed to load OCI bundle at \(bundlePath): \(error)")
|
||||
}
|
||||
try execInNamespace(spec: spec, log: log)
|
||||
} catch {
|
||||
App.writeError(error)
|
||||
throw error
|
||||
@@ -146,12 +151,54 @@ struct RunCommand: ParsableCommand {
|
||||
try App.exec(process: process, currentEnv: process.env)
|
||||
}
|
||||
|
||||
private func setupNamespaces(namespaces: [ContainerizationOCI.LinuxNamespace]?) throws -> Int32 {
|
||||
var unshareFlags: Int32 = 0
|
||||
|
||||
// Map namespace types to their corresponding CLONE flags
|
||||
let nsTypeToFlag: [ContainerizationOCI.LinuxNamespaceType: Int32] = [
|
||||
.pid: CLONE_NEWPID,
|
||||
.mount: CLONE_NEWNS,
|
||||
.uts: CLONE_NEWUTS,
|
||||
.ipc: CLONE_NEWIPC,
|
||||
.user: CLONE_NEWUSER,
|
||||
.cgroup: CLONE_NEWCGROUP,
|
||||
]
|
||||
|
||||
guard let namespaces = namespaces else {
|
||||
return CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUTS
|
||||
}
|
||||
|
||||
for ns in namespaces {
|
||||
guard let flag = nsTypeToFlag[ns.type] else {
|
||||
continue
|
||||
}
|
||||
|
||||
if ns.path.isEmpty {
|
||||
unshareFlags |= flag
|
||||
} else {
|
||||
let fd = open(ns.path, O_RDONLY | O_CLOEXEC)
|
||||
guard fd >= 0 else {
|
||||
throw App.Errno(stage: "open(\(ns.path))")
|
||||
}
|
||||
defer { close(fd) }
|
||||
|
||||
guard setns(fd, flag) == 0 else {
|
||||
throw App.Errno(stage: "setns(\(ns.path))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unshareFlags
|
||||
}
|
||||
|
||||
private func execInNamespace(spec: ContainerizationOCI.Spec, log: Logger) throws {
|
||||
let syncPipe = FileHandle(fileDescriptor: 3)
|
||||
let ackPipe = FileHandle(fileDescriptor: 4)
|
||||
|
||||
guard unshare(CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUTS) == 0 else {
|
||||
throw App.Errno(stage: "unshare(pid|mnt|uts)")
|
||||
let unshareFlags = try setupNamespaces(namespaces: spec.linux?.namespaces)
|
||||
|
||||
guard unshare(unshareFlags) == 0 else {
|
||||
throw App.Errno(stage: "unshare(\(unshareFlags))")
|
||||
}
|
||||
|
||||
let processID = fork()
|
||||
|
||||
@@ -428,7 +428,6 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
|
||||
"stdin": "Port: \(request.stdin)",
|
||||
"stdout": "Port: \(request.stdout)",
|
||||
"stderr": "Port: \(request.stderr)",
|
||||
"configuration": "\(request.configuration.count)",
|
||||
])
|
||||
|
||||
if !request.hasContainerID {
|
||||
|
||||
Reference in New Issue
Block a user