Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Sources/Containerization/CreateProcessOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//

public struct CreateProcessOptions: Sendable, Codable {
/// The process is created outside containerized env.
public var native: Bool

enum CodingKeys: String, CodingKey {
case native
}

public init(native: Bool) {
self.native = native
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

native = try container.decodeIfPresent(Bool.self, forKey: .native) ?? false
}
}
Comment on lines +17 to +34
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createProcess API takes in an optional containerID. This is what I was going to use to signify that we'd want to run a process in the root of the VM. e.g. supplying a nil containerID.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer we don't introduce new public API like this without really thinking through it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I forgot. Yeah let me update.
Do you think NativeProcess should live outside of ManagedContainer?

This will need reimplementing bookkeeping structure and management logic for NativeProcess.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think NativeProcess should live outside of ManagedContainer?

Whatever you think is right. If it feels too much hassle let me know

6 changes: 4 additions & 2 deletions Sources/Containerization/LinuxContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,7 @@ extension LinuxContainer {
public func kill(_ signal: Signal) async throws {
try await self.state.withLock {
let state = try $0.startedState("kill")

try await state.process.kill(signal)
}
}
Expand Down Expand Up @@ -915,8 +916,8 @@ extension LinuxContainer {
}

/// Execute a new process in the container. The process is not started after this call, and must be manually started
/// via the `start` method.
public func exec(_ id: String, configuration: LinuxProcessConfiguration) async throws -> LinuxProcess {
/// via the `start` method. When `native` is true, the process is created outside of container, running in the root of VM.
public func exec(_ id: String, configuration: LinuxProcessConfiguration, native: Bool = false) async throws -> LinuxProcess {
Comment thread
JaewonHur marked this conversation as resolved.
try await self.state.withLock {
var state = try $0.startedState("exec")

Expand All @@ -935,6 +936,7 @@ extension LinuxContainer {
containerID: self.id,
spec: spec,
io: stdio,
native: native,
ociRuntimePath: self.config.ociRuntimePath,
agent: agent,
vm: state.vm,
Expand Down
10 changes: 9 additions & 1 deletion Sources/Containerization/LinuxProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public final class LinuxProcess: Sendable {

private let state: Mutex<State>
private let ioSetup: Stdio
private let native: Bool
private let agent: any VirtualMachineAgent
private let vm: any VirtualMachineInstance
private let ociRuntimePath: String?
Expand All @@ -105,6 +106,7 @@ public final class LinuxProcess: Sendable {
containerID: String? = nil,
spec: Spec,
io: Stdio,
native: Bool = false,
ociRuntimePath: String?,
agent: any VirtualMachineAgent,
vm: any VirtualMachineInstance,
Expand All @@ -115,6 +117,7 @@ public final class LinuxProcess: Sendable {
self.owningContainer = containerID
self.state = Mutex<State>(.init(spec: spec, pid: -1, stdio: StdioHandles()))
self.ioSetup = io
self.native = native
self.agent = agent
self.ociRuntimePath = ociRuntimePath
self.vm = vm
Expand Down Expand Up @@ -240,6 +243,11 @@ extension LinuxProcess {
do {
let spec = self.state.withLock { $0.spec }
var listeners = [VsockListener?](repeating: nil, count: 3)

let options = try JSONEncoder().encode(
CreateProcessOptions(native: self.native)
)

if let stdin = self.ioSetup.stdin {
listeners[0] = try self.vm.listen(stdin.port)
}
Expand Down Expand Up @@ -268,7 +276,7 @@ extension LinuxProcess {
stderrPort: self.ioSetup.stderr?.port,
ociRuntimePath: self.ociRuntimePath,
configuration: spec,
options: nil
options: options
)

let result = try await t.value
Expand Down
3 changes: 3 additions & 0 deletions Sources/Containerization/Vminitd.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ extension Vminitd: VirtualMachineAgent {
if let ociRuntimePath {
$0.ociRuntimePath = ociRuntimePath
}
if let options {
$0.options = options
}
$0.configuration = try enc.encode(configuration)
})
}
Expand Down
29 changes: 20 additions & 9 deletions vminitd/Sources/VminitdCore/ManagedContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ extension ManagedContainer {
func createExec(
id: String,
stdio: HostStdio,
process: ContainerizationOCI.Process
process: ContainerizationOCI.Process,
native: Bool
) throws {
log.debug("creating exec process with \(process)")

Expand All @@ -170,14 +171,24 @@ extension ManagedContainer {
id: id,
process: process
)
let process = try ManagedProcess(
id: id,
stdio: stdio,
bundle: self.bundle,
owningPid: self.initProcess.pid,
log: self.log
)
self.execs[id] = process
let exec: ContainerProcess
if native {
exec = try NativeProcess(
id: id,
stdio: stdio,
process: process,
log: self.log
)
} else {
exec = try ManagedProcess(
id: id,
stdio: stdio,
bundle: self.bundle,
owningPid: self.initProcess.pid,
log: self.log
)
}
self.execs[id] = exec
}

func start(execID: String) async throws -> Int32 {
Expand Down
206 changes: 206 additions & 0 deletions vminitd/Sources/VminitdCore/NativeProcess.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//

import ContainerizationError
import ContainerizationOCI
import ContainerizationOS
import Foundation
import Logging
import Synchronization
import SystemPackage

final class NativeProcess: ContainerProcess, Sendable {
private struct State {
init(io: ManagedProcess.IO) {
self.io = io
}

var waiters: [CheckedContinuation<ContainerExitStatus, Never>] = []
var exitStatus: ContainerExitStatus? = nil
var pid: Int32?
let io: ManagedProcess.IO
}

let id: String

private let log: Logger
private let command: Command
private let state: Mutex<State>

var pid: Int32? {
self.state.withLock {
$0.pid
}
}

init(
id: String,
stdio: HostStdio,
process: ContainerizationOCI.Process,
log: Logger
) throws {
self.id = id
var log = log
log[metadataKey: "id"] = "\(id)"
self.log = log

guard !process.args.isEmpty else {
throw ContainerizationError(.invalidArgument, message: "process args cannot be empty")
}

let executableArg = process.args[0]
guard executableArg.hasPrefix("/") else {
throw ContainerizationError(.invalidArgument, message: "executable path must be absolute path")
}

let executable = FilePath(executableArg)
guard FileManager.default.fileExists(atPath: executable.string) else {
throw ContainerizationError(.invalidArgument, message: "failed to find target executable \(executableArg)")
}

var command = Command(
executable.string,
arguments: Array(process.args.dropFirst()),
environment: process.env,
directory: process.cwd
)

guard !stdio.terminal else {
throw ContainerizationError(.invalidArgument, message: "native process doesn't support terminal")
}

command.attrs = .init(setsid: false)
let io = StandardIO(
stdio: stdio,
log: log
)

log.info("starting I/O")

// Setup IO early. We expect the host to be listening already.
try io.start(process: &command)

self.command = command
self.state = Mutex(State(io: io))
}

func start() async throws -> Int32 {
do {
return try self.state.withLock {
log.info(
"starting native process",
metadata: ["id": "\(id)"]
)

try command.start()
try $0.io.closeAfterExec()

let pid = command.pid
$0.pid = pid

log.info(
"started native process",
metadata: [
"pid": "\(pid)",
"id": "\(id)",
]
)

return pid
}
} catch {
throw ContainerizationError(
.internalError,
message: "native process failed to start: \(error)"
)
}
}

func setExit(_ status: Int32) {
self.state.withLock { state in
self.log.info(
"native process exit",
metadata: [
"status": "\(status)"
]
)

let exitStatus = ContainerExitStatus(exitCode: status, exitedAt: Date.now)
state.exitStatus = exitStatus

do {
try state.io.close()
} catch {
self.log.error("failed to close I/O for process: \(error)")
}

for waiter in state.waiters {
waiter.resume(returning: exitStatus)
}

self.log.debug("\(state.waiters.count) native process waiters signaled")
state.waiters.removeAll()
}
}

func wait() async -> ContainerExitStatus {
await withCheckedContinuation { cont in
self.state.withLock {
if let status = $0.exitStatus {
cont.resume(returning: status)
return
}
$0.waiters.append(cont)
}
}
}

func kill(_ signal: Int32) async throws {
try self.state.withLock {
guard let pid = $0.pid else {
throw ContainerizationError(.invalidState, message: "process PID is required")
}

guard $0.exitStatus == nil else {
return
}

self.log.info("sending signal \(signal) to native process \(pid)")
guard Foundation.kill(pid, signal) == 0 else {
throw POSIXError.fromErrno()
}
}
}

func resize(size: Terminal.Size) throws {
try self.state.withLock {
guard $0.exitStatus == nil else {
return
}
try $0.io.resize(size: size)
}
}

func closeStdin() throws {
let io = self.state.withLock { $0.io }
try io.closeStdin()
}

func delete() async throws {
// Nothing to be done
}

}
8 changes: 7 additions & 1 deletion vminitd/Sources/VminitdCore/Server+GRPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -789,12 +789,18 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ
terminal: process.terminal
)

let options = try JSONDecoder().decode(
CreateProcessOptions.self,
from: request.options
)

// This is an exec.
if let container = await self.state.containers[request.containerID] {
try await container.createExec(
id: request.id,
stdio: stdioPorts,
process: process
process: process,
native: options.native
)
} else {
// We need to make our new fangled container.
Expand Down
Loading