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:
Danny Canter
2025-12-05 10:42:50 -05:00
committed by GitHub
parent c45fef7278
commit 2d27ef6deb
5 changed files with 186 additions and 7 deletions
+89 -1
View File
@@ -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,
+44
View File
@@ -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)'")
}
}
}
+1
View File
@@ -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)
+52 -5
View File
@@ -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 {