Skip to content
Merged
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
193 changes: 190 additions & 3 deletions Sources/Containerization/LinuxProcessConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,193 @@
import ContainerizationOCI
import ContainerizationOS

/// A resource limit (rlimit) configuration for a container process.
public struct LinuxRLimit: Sendable, Hashable {
/// The kind of resource limit.
public var kind: Kind
/// The hard limit value.
public var hard: UInt64
/// The soft limit value.
public var soft: UInt64

/// Creates a new resource limit.
///
/// - Parameters:
/// - kind: The kind of resource limit.
/// - hard: The hard limit value.
/// - soft: The soft limit value.
public init(kind: Kind, hard: UInt64, soft: UInt64) {
self.kind = kind
self.hard = hard
self.soft = soft
}

/// Creates a new resource limit with the same value for both hard and soft limits.
///
/// - Parameters:
/// - kind: The kind of resource limit.
/// - limit: The limit value for both hard and soft limits.
public init(kind: Kind, limit: UInt64) {
self.kind = kind
self.hard = limit
self.soft = limit
}

/// Convert to OCI POSIXRlimit format for transport.
public func toOCI() -> POSIXRlimit {
POSIXRlimit(type: self.kind.description, hard: self.hard, soft: self.soft)
}
}

extension LinuxRLimit {
/// The kind of resource limit.
public struct Kind: Sendable, Hashable {
private enum Value: Hashable, Sendable, CaseIterable {
case addressSpace
case coreFileSize
case cpuTime
case dataSize
case fileSize
case locks
case lockedMemory
case messageQueue
case nice
case openFiles
case numberOfProcesses
case residentSetSize
case realtimePriority
case realtimeTimeout
case signalsPending
case stackSize
}

private var value: Value
private init(_ value: Value) {
self.value = value
}

/// Maximum size of the process's virtual memory (address space) in bytes.
public static var addressSpace: Self {
Self(.addressSpace)
}

/// Maximum size of a core file in bytes.
public static var coreFileSize: Self {
Self(.coreFileSize)
}

/// Maximum amount of CPU time the process can consume in seconds.
public static var cpuTime: Self {
Self(.cpuTime)
}

/// Maximum size of the process's data segment in bytes.
public static var dataSize: Self {
Self(.dataSize)
}

/// Maximum size of files the process may create in bytes.
public static var fileSize: Self {
Self(.fileSize)
}

/// Maximum number of file locks.
public static var locks: Self {
Self(.locks)
}

/// Maximum number of bytes of memory that may be locked into RAM.
public static var lockedMemory: Self {
Self(.lockedMemory)
}

/// Maximum number of bytes that can be allocated for POSIX message queues.
public static var messageQueue: Self {
Self(.messageQueue)
}

/// Maximum nice value that can be set.
public static var nice: Self {
Self(.nice)
}

/// Maximum number of open file descriptors.
public static var openFiles: Self {
Self(.openFiles)
}

/// Maximum number of processes that can be created by the user.
public static var numberOfProcesses: Self {
Self(.numberOfProcesses)
}

/// Maximum size of the process's resident set (physical memory) in bytes.
public static var residentSetSize: Self {
Self(.residentSetSize)
}

/// Maximum real-time scheduling priority.
public static var realtimePriority: Self {
Self(.realtimePriority)
}

/// Maximum amount of CPU time for real-time scheduling in microseconds.
public static var realtimeTimeout: Self {
Self(.realtimeTimeout)
}

/// Maximum number of signals that may be queued.
public static var signalsPending: Self {
Self(.signalsPending)
}

/// Maximum size of the process stack in bytes.
public static var stackSize: Self {
Self(.stackSize)
}
}
}

extension LinuxRLimit.Kind: CustomStringConvertible {
/// The OCI string representation of the resource limit kind.
public var description: String {
switch self.value {
case .addressSpace:
"RLIMIT_AS"
case .coreFileSize:
"RLIMIT_CORE"
case .cpuTime:
"RLIMIT_CPU"
case .dataSize:
"RLIMIT_DATA"
case .fileSize:
"RLIMIT_FSIZE"
case .locks:
"RLIMIT_LOCKS"
case .lockedMemory:
"RLIMIT_MEMLOCK"
case .messageQueue:
"RLIMIT_MSGQUEUE"
case .nice:
"RLIMIT_NICE"
case .openFiles:
"RLIMIT_NOFILE"
case .numberOfProcesses:
"RLIMIT_NPROC"
case .residentSetSize:
"RLIMIT_RSS"
case .realtimePriority:
"RLIMIT_RTPRIO"
case .realtimeTimeout:
"RLIMIT_RTTIME"
case .signalsPending:
"RLIMIT_SIGPENDING"
case .stackSize:
"RLIMIT_STACK"
}
}
}

/// User-friendly Linux capabilities configuration
public struct LinuxCapabilities: Sendable {
/// Capabilities that define the maximum set of capabilities a process can have
Expand Down Expand Up @@ -140,7 +327,7 @@ public struct LinuxProcessConfiguration: Sendable {
/// The user the container process will run as.
public var user: ContainerizationOCI.User = .init()
/// The rlimits for the container process.
public var rlimits: [POSIXRlimit] = []
public var rlimits: [LinuxRLimit] = []
/// The Linux capabilities for the container process.
public var capabilities: LinuxCapabilities = .allCapabilities
/// Whether to allocate a pseudo terminal for the process. If you'd like interactive
Expand All @@ -161,7 +348,7 @@ public struct LinuxProcessConfiguration: Sendable {
environmentVariables: [String] = ["PATH=\(Self.defaultPath)"],
workingDirectory: String = "/",
user: ContainerizationOCI.User = .init(),
rlimits: [POSIXRlimit] = [],
rlimits: [LinuxRLimit] = [],
capabilities: LinuxCapabilities = .allCapabilities,
terminal: Bool = false,
stdin: ReaderStream? = nil,
Expand Down Expand Up @@ -208,7 +395,7 @@ public struct LinuxProcessConfiguration: Sendable {
env: self.environmentVariables,
capabilities: self.capabilities.toOCI(),
user: self.user,
rlimits: self.rlimits,
rlimits: self.rlimits.map { $0.toOCI() },
terminal: self.terminal
)
}
Expand Down
135 changes: 135 additions & 0 deletions Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2317,4 +2317,139 @@ extension IntegrationSuite {
throw error
}
}

func testRLimitOpenFiles() async throws {
let id = "test-rlimit-open-files"

let bs = try await bootstrap(id)
let buffer = BufferWriter()
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sh", "-c", "ulimit -n"]
config.process.rlimits = [
LinuxRLimit(kind: .openFiles, hard: 2048, soft: 1024)
]
config.process.stdout = buffer
config.bootLog = bs.bootLog
}

try await container.create()
try await container.start()

let status = try await container.wait()
try await container.stop()

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}

guard let output = String(data: buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}

// ulimit -n returns the soft limit
guard output == "1024" else {
throw IntegrationError.assert(msg: "expected soft limit '1024', got '\(output)'")
}
}

func testRLimitMultiple() async throws {
let id = "test-rlimit-multiple"

let bs = try await bootstrap(id)
let buffer = BufferWriter()
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
// Read /proc/self/limits to verify multiple rlimits are set
config.process.arguments = ["cat", "/proc/self/limits"]
config.process.rlimits = [
LinuxRLimit(kind: .openFiles, hard: 4096, soft: 2048),
LinuxRLimit(kind: .stackSize, hard: 16_777_216, soft: 8_388_608),
LinuxRLimit(kind: .coreFileSize, hard: 0, soft: 0),
]
config.process.stdout = buffer
config.bootLog = bs.bootLog
}

try await container.create()
try await container.start()

let status = try await container.wait()
try await container.stop()

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}

guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}

// Parse /proc/self/limits and verify the values
// Format: "Limit Name Soft Limit Hard Limit Units"
let lines = output.split(separator: "\n")

// Helper to find and verify a limit line
func verifyLimit(name: String, expectedSoft: String, expectedHard: String) throws {
guard let line = lines.first(where: { $0.contains(name) }) else {
throw IntegrationError.assert(msg: "limit '\(name)' not found in output")
}
let parts = line.split(whereSeparator: { $0.isWhitespace }).map(String.init)
// The line format varies, but soft and hard are typically the last numeric values before units
guard parts.contains(expectedSoft) && parts.contains(expectedHard) else {
throw IntegrationError.assert(
msg: "limit '\(name)' expected soft=\(expectedSoft) hard=\(expectedHard), got: \(line)")
}
}

try verifyLimit(name: "Max open files", expectedSoft: "2048", expectedHard: "4096")
try verifyLimit(name: "Max stack size", expectedSoft: "8388608", expectedHard: "16777216")
try verifyLimit(name: "Max core file size", expectedSoft: "0", expectedHard: "0")
}

func testRLimitExec() async throws {
let id = "test-rlimit-exec"

let bs = try await bootstrap(id)
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
config.bootLog = bs.bootLog
}

do {
try await container.create()
try await container.start()

// Exec a process with rlimits set
let buffer = BufferWriter()
let exec = try await container.exec("rlimit-exec") { config in
config.arguments = ["sh", "-c", "ulimit -n"]
config.rlimits = [
LinuxRLimit(kind: .openFiles, hard: 512, soft: 256)
]
config.stdout = buffer
}

try await exec.start()
let status = try await exec.wait()
try await exec.delete()

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "exec status \(status) != 0")
}

guard let output = String(data: buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}

guard output == "256" else {
throw IntegrationError.assert(msg: "expected soft limit '256', got '\(output)'")
}

try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
}
Loading
Loading