Add capabilities support (#444)

Closes https://github.com/apple/containerization/issues/442

This adds capabilities support to LinuxContainer via a new surface in
ContainerizationOS + some C wrappers.
This commit is contained in:
Danny Canter authored and GitHub committed 2025-12-11 15:07:17 -05:00
1 parent 96d37e2e21
commit 7962dae643
13 files changed
+1207 -10

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright © 2025 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.
*/
#if defined(__linux__)
#include <sys/syscall.h>
#include <unistd.h>
#include "capability.h"
// Capability syscall wrappers
int CZ_capget(void *header, void *data) {
return syscall(SYS_capget, header, data);
}
int CZ_capset(void *header, void *data) {
return syscall(SYS_capset, header, data);
}
#endif
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright © 2025 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.
*/
#ifndef __CAPABILITY_H
#define __CAPABILITY_H
#if defined(__linux__)
// Capability syscall wrappers
int CZ_capget(void *header, void *data);
int CZ_capset(void *header, void *data);
#endif
#endif
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright © 2025 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.
*/
#ifndef __PRCTL_H
#define __PRCTL_H
#if defined(__linux__)
#include <sys/types.h>
// Capability management prctl wrappers
int CZ_prctl_set_keepcaps();
int CZ_prctl_clear_keepcaps();
int CZ_prctl_capbset_drop(unsigned int capability);
int CZ_prctl_cap_ambient_clear_all();
int CZ_prctl_cap_ambient_raise(unsigned int capability);
#endif
#endif
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright © 2025 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.
*/
#if defined(__linux__)
#include <sys/prctl.h>
#include "prctl.h"
// Set keep caps to preserve capabilities across setuid()
int CZ_prctl_set_keepcaps() {
return prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0);
}
// Clear keep caps after user change
int CZ_prctl_clear_keepcaps() {
return prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0);
}
// Drop capability from bounding set
int CZ_prctl_capbset_drop(unsigned int capability) {
return prctl(PR_CAPBSET_DROP, capability, 0, 0, 0);
}
// Clear all ambient capabilities
int CZ_prctl_cap_ambient_clear_all() {
return prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
}
// Raise ambient capability
int CZ_prctl_cap_ambient_raise(unsigned int capability) {
return prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, capability, 0, 0);
}
#endif
@@ -17,6 +17,116 @@
import ContainerizationOCI
import ContainerizationOS
/// User-friendly Linux capabilities configuration
public struct LinuxCapabilities: Sendable {
/// Capabilities that define the maximum set of capabilities a process can have
public var bounding: [CapabilityName] = []
/// Capabilities that are actually in effect for the current process
public var effective: [CapabilityName] = []
/// Capabilities that can be inherited by child processes
public var inheritable: [CapabilityName] = []
/// Capabilities that are currently permitted for the process
public var permitted: [CapabilityName] = []
/// Capabilities that are preserved across execve() calls
public var ambient: [CapabilityName] = []
/// Grant all capabilities
public static let allCapabilities = LinuxCapabilities(
bounding: CapabilityName.allCases,
effective: CapabilityName.allCases,
inheritable: CapabilityName.allCases,
permitted: CapabilityName.allCases,
ambient: CapabilityName.allCases
)
/// Default configuration
public static let defaultOCICapabilities = LinuxCapabilities(
bounding: [
.chown,
.dacOverride,
.fsetid,
.fowner,
.mknod,
.netRaw,
.setgid,
.setuid,
.setfcap,
.setpcap,
.netBindService,
.sysChroot,
.kill,
.auditWrite,
],
effective: [
.chown,
.dacOverride,
.fsetid,
.fowner,
.mknod,
.netRaw,
.setgid,
.setuid,
.setfcap,
.setpcap,
.netBindService,
.sysChroot,
.kill,
.auditWrite,
],
permitted: [
.chown,
.dacOverride,
.fsetid,
.fowner,
.mknod,
.netRaw,
.setgid,
.setuid,
.setfcap,
.setpcap,
.netBindService,
.sysChroot,
.kill,
.auditWrite,
],
)
public init(
bounding: [CapabilityName] = [],
effective: [CapabilityName] = [],
inheritable: [CapabilityName] = [],
permitted: [CapabilityName] = [],
ambient: [CapabilityName] = []
) {
self.bounding = bounding
self.effective = effective
self.inheritable = inheritable
self.permitted = permitted
self.ambient = ambient
}
/// Convenience initializer that sets the same capabilities to effective, permitted, and bounding sets
/// This matches the typical pattern used by containerd/runc
public init(capabilities: [CapabilityName]) {
self.bounding = capabilities
self.effective = capabilities
self.inheritable = []
self.permitted = capabilities
self.ambient = []
}
/// Convert to OCI format for transport
public func toOCI() -> ContainerizationOCI.LinuxCapabilities {
ContainerizationOCI.LinuxCapabilities(
bounding: bounding.isEmpty ? nil : bounding.map { $0.description },
effective: effective.isEmpty ? nil : effective.map { $0.description },
inheritable: inheritable.isEmpty ? nil : inheritable.map { $0.description },
permitted: permitted.isEmpty ? nil : permitted.map { $0.description },
ambient: ambient.isEmpty ? nil : ambient.map { $0.description }
)
}
}
public struct LinuxProcessConfiguration: Sendable {
/// The default PATH value for a process.
public static let defaultPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
@@ -31,6 +141,8 @@ public struct LinuxProcessConfiguration: Sendable {
public var user: ContainerizationOCI.User = .init()
/// The rlimits for the container process.
public var rlimits: [POSIXRlimit] = []
/// 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
/// behavior and are planning to use a terminal for stdin/out/err on the client side,
/// this should likely be set to true.
@@ -50,6 +162,7 @@ public struct LinuxProcessConfiguration: Sendable {
workingDirectory: String = "/",
user: ContainerizationOCI.User = .init(),
rlimits: [POSIXRlimit] = [],
capabilities: LinuxCapabilities = .allCapabilities,
terminal: Bool = false,
stdin: ReaderStream? = nil,
stdout: Writer? = nil,
@@ -60,6 +173,7 @@ public struct LinuxProcessConfiguration: Sendable {
self.workingDirectory = workingDirectory
self.user = user
self.rlimits = rlimits
self.capabilities = capabilities
self.terminal = terminal
self.stdin = stdin
self.stdout = stdout
@@ -92,6 +206,7 @@ public struct LinuxProcessConfiguration: Sendable {
args: self.arguments,
cwd: self.workingDirectory,
env: self.environmentVariables,
capabilities: self.capabilities.toOCI(),
user: self.user,
rlimits: self.rlimits,
terminal: self.terminal
+37 -10
View File
@@ -157,6 +157,7 @@ public struct Process: Codable, Sendable {
}()
self.init(args: args, cwd: cwd, env: env, user: user)
}
public init(from decoder: Decoder) throws {
self.init()
@@ -194,18 +195,26 @@ public struct Process: Codable, Sendable {
}
public struct LinuxCapabilities: Codable, Sendable {
public var bounding: [String]
public var effective: [String]
public var inheritable: [String]
public var permitted: [String]
public var ambient: [String]
public var bounding: [String]?
public var effective: [String]?
public var inheritable: [String]?
public var permitted: [String]?
public var ambient: [String]?
enum CodingKeys: String, CodingKey {
case bounding
case effective
case inheritable
case permitted
case ambient
}
public init(
bounding: [String],
effective: [String],
inheritable: [String],
permitted: [String],
ambient: [String]
bounding: [String]? = nil,
effective: [String]? = nil,
inheritable: [String]? = nil,
permitted: [String]? = nil,
ambient: [String]? = nil
) {
self.bounding = bounding
self.effective = effective
@@ -213,6 +222,24 @@ public struct LinuxCapabilities: Codable, Sendable {
self.permitted = permitted
self.ambient = ambient
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.bounding = try container.decodeIfPresent([String].self, forKey: .bounding)
self.effective = try container.decodeIfPresent([String].self, forKey: .effective)
self.inheritable = try container.decodeIfPresent([String].self, forKey: .inheritable)
self.permitted = try container.decodeIfPresent([String].self, forKey: .permitted)
self.ambient = try container.decodeIfPresent([String].self, forKey: .ambient)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(bounding, forKey: .bounding)
try container.encodeIfPresent(effective, forKey: .effective)
try container.encodeIfPresent(inheritable, forKey: .inheritable)
try container.encodeIfPresent(permitted, forKey: .permitted)
try container.encodeIfPresent(ambient, forKey: .ambient)
}
}
public struct Box: Codable, Sendable {
@@ -0,0 +1,651 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 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 CShim
import Foundation
// MARK: - Configuration Types
public struct CapabilitySet: Sendable, Hashable {
private enum Value: Hashable, Sendable, CaseIterable {
case bounding
case effective
case inheritable
case permitted
case ambient
}
private var value: Value
private init(_ value: Value) {
self.value = value
}
public init(rawValue: String) {
let values = Value.allCases.reduce(into: [String: Value]()) {
$0[String(describing: $1)] = $1
}
let match = values[rawValue]
guard let match else {
fatalError("invalid CapabilitySet Value \(rawValue)")
}
self.value = match
}
public static var bounding: Self { Self(.bounding) }
public static var effective: Self { Self(.effective) }
public static var inheritable: Self { Self(.inheritable) }
public static var permitted: Self { Self(.permitted) }
public static var ambient: Self { Self(.ambient) }
}
extension CapabilitySet: CustomStringConvertible {
public var description: String {
String(describing: self.value)
}
}
public struct CapabilityName: Sendable, Hashable {
private enum Value: Hashable, Sendable, CaseIterable {
case chown
case dacOverride
case dacReadSearch
case fowner
case fsetid
case kill
case setgid
case setuid
case setpcap
case linuxImmutable
case netBindService
case netBroadcast
case netAdmin
case netRaw
case ipcLock
case ipcOwner
case sysModule
case sysRawio
case sysChroot
case sysPtrace
case sysPacct
case sysAdmin
case sysBoot
case sysNice
case sysResource
case sysTime
case sysTtyConfig
case mknod
case lease
case auditWrite
case auditControl
case setfcap
case macOverride
case macAdmin
case syslog
case wakeAlarm
case blockSuspend
case auditRead
case perfmon
case bpf
case checkpointRestore
}
private var value: Value
private init(_ value: Value) {
self.value = value
}
public init(rawValue: String) {
let normalized = rawValue.hasPrefix("CAP_") ? rawValue : "CAP_\(rawValue)"
let capNameMap: [String: Value] = [
"CAP_CHOWN": .chown,
"CAP_DAC_OVERRIDE": .dacOverride,
"CAP_DAC_READ_SEARCH": .dacReadSearch,
"CAP_FOWNER": .fowner,
"CAP_FSETID": .fsetid,
"CAP_KILL": .kill,
"CAP_SETGID": .setgid,
"CAP_SETUID": .setuid,
"CAP_SETPCAP": .setpcap,
"CAP_LINUX_IMMUTABLE": .linuxImmutable,
"CAP_NET_BIND_SERVICE": .netBindService,
"CAP_NET_BROADCAST": .netBroadcast,
"CAP_NET_ADMIN": .netAdmin,
"CAP_NET_RAW": .netRaw,
"CAP_IPC_LOCK": .ipcLock,
"CAP_IPC_OWNER": .ipcOwner,
"CAP_SYS_MODULE": .sysModule,
"CAP_SYS_RAWIO": .sysRawio,
"CAP_SYS_CHROOT": .sysChroot,
"CAP_SYS_PTRACE": .sysPtrace,
"CAP_SYS_PACCT": .sysPacct,
"CAP_SYS_ADMIN": .sysAdmin,
"CAP_SYS_BOOT": .sysBoot,
"CAP_SYS_NICE": .sysNice,
"CAP_SYS_RESOURCE": .sysResource,
"CAP_SYS_TIME": .sysTime,
"CAP_SYS_TTY_CONFIG": .sysTtyConfig,
"CAP_MKNOD": .mknod,
"CAP_LEASE": .lease,
"CAP_AUDIT_WRITE": .auditWrite,
"CAP_AUDIT_CONTROL": .auditControl,
"CAP_SETFCAP": .setfcap,
"CAP_MAC_OVERRIDE": .macOverride,
"CAP_MAC_ADMIN": .macAdmin,
"CAP_SYSLOG": .syslog,
"CAP_WAKE_ALARM": .wakeAlarm,
"CAP_BLOCK_SUSPEND": .blockSuspend,
"CAP_AUDIT_READ": .auditRead,
"CAP_PERFMON": .perfmon,
"CAP_BPF": .bpf,
"CAP_CHECKPOINT_RESTORE": .checkpointRestore,
]
guard let match = capNameMap[normalized] else {
fatalError("invalid CapabilityName \(normalized)")
}
self.value = match
}
public var capValue: UInt32 {
switch self.value {
case .chown: return 0
case .dacOverride: return 1
case .dacReadSearch: return 2
case .fowner: return 3
case .fsetid: return 4
case .kill: return 5
case .setgid: return 6
case .setuid: return 7
case .setpcap: return 8
case .linuxImmutable: return 9
case .netBindService: return 10
case .netBroadcast: return 11
case .netAdmin: return 12
case .netRaw: return 13
case .ipcLock: return 14
case .ipcOwner: return 15
case .sysModule: return 16
case .sysRawio: return 17
case .sysChroot: return 18
case .sysPtrace: return 19
case .sysPacct: return 20
case .sysAdmin: return 21
case .sysBoot: return 22
case .sysNice: return 23
case .sysResource: return 24
case .sysTime: return 25
case .sysTtyConfig: return 26
case .mknod: return 27
case .lease: return 28
case .auditWrite: return 29
case .auditControl: return 30
case .setfcap: return 31
case .macOverride: return 32
case .macAdmin: return 33
case .syslog: return 34
case .wakeAlarm: return 35
case .blockSuspend: return 36
case .auditRead: return 37
case .perfmon: return 38
case .bpf: return 39
case .checkpointRestore: return 40
}
}
public static var chown: Self { Self(.chown) }
public static var dacOverride: Self { Self(.dacOverride) }
public static var dacReadSearch: Self { Self(.dacReadSearch) }
public static var fowner: Self { Self(.fowner) }
public static var fsetid: Self { Self(.fsetid) }
public static var kill: Self { Self(.kill) }
public static var setgid: Self { Self(.setgid) }
public static var setuid: Self { Self(.setuid) }
public static var setpcap: Self { Self(.setpcap) }
public static var linuxImmutable: Self { Self(.linuxImmutable) }
public static var netBindService: Self { Self(.netBindService) }
public static var netBroadcast: Self { Self(.netBroadcast) }
public static var netAdmin: Self { Self(.netAdmin) }
public static var netRaw: Self { Self(.netRaw) }
public static var ipcLock: Self { Self(.ipcLock) }
public static var ipcOwner: Self { Self(.ipcOwner) }
public static var sysModule: Self { Self(.sysModule) }
public static var sysRawio: Self { Self(.sysRawio) }
public static var sysChroot: Self { Self(.sysChroot) }
public static var sysPtrace: Self { Self(.sysPtrace) }
public static var sysPacct: Self { Self(.sysPacct) }
public static var sysAdmin: Self { Self(.sysAdmin) }
public static var sysBoot: Self { Self(.sysBoot) }
public static var sysNice: Self { Self(.sysNice) }
public static var sysResource: Self { Self(.sysResource) }
public static var sysTime: Self { Self(.sysTime) }
public static var sysTtyConfig: Self { Self(.sysTtyConfig) }
public static var mknod: Self { Self(.mknod) }
public static var lease: Self { Self(.lease) }
public static var auditWrite: Self { Self(.auditWrite) }
public static var auditControl: Self { Self(.auditControl) }
public static var setfcap: Self { Self(.setfcap) }
public static var macOverride: Self { Self(.macOverride) }
public static var macAdmin: Self { Self(.macAdmin) }
public static var syslog: Self { Self(.syslog) }
public static var wakeAlarm: Self { Self(.wakeAlarm) }
public static var blockSuspend: Self { Self(.blockSuspend) }
public static var auditRead: Self { Self(.auditRead) }
public static var perfmon: Self { Self(.perfmon) }
public static var bpf: Self { Self(.bpf) }
public static var checkpointRestore: Self { Self(.checkpointRestore) }
public static var allCases: [CapabilityName] {
Value.allCases.map { CapabilityName($0) }
}
}
extension CapabilityName: CustomStringConvertible {
public var description: String {
switch self.value {
case .chown: return "CAP_CHOWN"
case .dacOverride: return "CAP_DAC_OVERRIDE"
case .dacReadSearch: return "CAP_DAC_READ_SEARCH"
case .fowner: return "CAP_FOWNER"
case .fsetid: return "CAP_FSETID"
case .kill: return "CAP_KILL"
case .setgid: return "CAP_SETGID"
case .setuid: return "CAP_SETUID"
case .setpcap: return "CAP_SETPCAP"
case .linuxImmutable: return "CAP_LINUX_IMMUTABLE"
case .netBindService: return "CAP_NET_BIND_SERVICE"
case .netBroadcast: return "CAP_NET_BROADCAST"
case .netAdmin: return "CAP_NET_ADMIN"
case .netRaw: return "CAP_NET_RAW"
case .ipcLock: return "CAP_IPC_LOCK"
case .ipcOwner: return "CAP_IPC_OWNER"
case .sysModule: return "CAP_SYS_MODULE"
case .sysRawio: return "CAP_SYS_RAWIO"
case .sysChroot: return "CAP_SYS_CHROOT"
case .sysPtrace: return "CAP_SYS_PTRACE"
case .sysPacct: return "CAP_SYS_PACCT"
case .sysAdmin: return "CAP_SYS_ADMIN"
case .sysBoot: return "CAP_SYS_BOOT"
case .sysNice: return "CAP_SYS_NICE"
case .sysResource: return "CAP_SYS_RESOURCE"
case .sysTime: return "CAP_SYS_TIME"
case .sysTtyConfig: return "CAP_SYS_TTY_CONFIG"
case .mknod: return "CAP_MKNOD"
case .lease: return "CAP_LEASE"
case .auditWrite: return "CAP_AUDIT_WRITE"
case .auditControl: return "CAP_AUDIT_CONTROL"
case .setfcap: return "CAP_SETFCAP"
case .macOverride: return "CAP_MAC_OVERRIDE"
case .macAdmin: return "CAP_MAC_ADMIN"
case .syslog: return "CAP_SYSLOG"
case .wakeAlarm: return "CAP_WAKE_ALARM"
case .blockSuspend: return "CAP_BLOCK_SUSPEND"
case .auditRead: return "CAP_AUDIT_READ"
case .perfmon: return "CAP_PERFMON"
case .bpf: return "CAP_BPF"
case .checkpointRestore: return "CAP_CHECKPOINT_RESTORE"
}
}
}
// MARK: - Linux Implementation
#if os(Linux)
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif
import CShim
/// Capability type flags
public struct CapType: OptionSet, Sendable {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
// Individual capability sets (for Get/Set/Unset/etc)
public static let effective = CapType(rawValue: 1 << 0)
public static let permitted = CapType(rawValue: 1 << 1)
public static let inheritable = CapType(rawValue: 1 << 2)
public static let bounding = CapType(rawValue: 1 << 3)
public static let ambient = CapType(rawValue: 1 << 4)
// Bulk operation flags (for Apply/Fill/Clear)
public static let caps = CapType(rawValue: 1 << 8) // CAPS - effective, permitted, inheritable
public static let bounds = CapType(rawValue: 1 << 9) // BOUNDS - bounding set
public static let ambs = CapType(rawValue: 1 << 10) // AMBS - ambient capabilities
}
private struct CapabilityHeader {
var version: UInt32
var pid: Int32
init(pid: Int32 = 0) {
self.version = 0x2008_0522
self.pid = pid
}
}
private struct CapabilityData {
var effective1: UInt32
var permitted1: UInt32
var inheritable1: UInt32
var effective2: UInt32
var permitted2: UInt32
var inheritable2: UInt32
init(
effective1: UInt32 = 0,
permitted1: UInt32 = 0,
inheritable1: UInt32 = 0,
effective2: UInt32 = 0,
permitted2: UInt32 = 0,
inheritable2: UInt32 = 0
) {
self.effective1 = effective1
self.permitted1 = permitted1
self.inheritable1 = inheritable1
self.effective2 = effective2
self.permitted2 = permitted2
self.inheritable2 = inheritable2
}
}
/// Interface with Linux capabilities
/// https://linux.die.net/man/7/capabilities
public struct LinuxCapabilities: Sendable {
private var effectiveSet: UInt64 = 0
private var permittedSet: UInt64 = 0
private var inheritableSet: UInt64 = 0
private var boundingSet: UInt64 = 0
private var ambientSet: UInt64 = 0
public init() {}
/// Get the highest supported capability from the kernel
public static func getLastSupported() throws -> CapabilityName {
guard let data = try? String(contentsOfFile: "/proc/sys/kernel/cap_last_cap", encoding: .ascii),
let lastCap = UInt32(data.trimmingCharacters(in: .whitespacesAndNewlines))
else {
throw LinuxCapabilities.Error.invalidCapabilitySet("failed to read /proc/sys/kernel/cap_last_cap")
}
guard let capability = CapabilityName.allCases.first(where: { $0.capValue == lastCap }) else {
throw LinuxCapabilities.Error.invalidCapabilitySet("no capability found for kernel max cap \(lastCap)")
}
return capability
}
/// Set keep caps
public static func setKeepCaps() throws {
let result = CZ_prctl_set_keepcaps()
if result != 0 {
throw LinuxCapabilities.Error.prctlFailed(errno: errno, operation: "PR_SET_KEEPCAPS")
}
}
/// Clear keep caps
public static func clearKeepCaps() throws {
let result = CZ_prctl_clear_keepcaps()
if result != 0 {
throw LinuxCapabilities.Error.prctlFailed(errno: errno, operation: "PR_CLEAR_KEEPCAPS")
}
}
/// Load current process capabilities from kernel
public mutating func load() throws {
let data = try getCurrentCapabilities()
self.effectiveSet = UInt64(data.effective1)
self.permittedSet = UInt64(data.permitted1)
self.inheritableSet = UInt64(data.inheritable1)
}
/// Check if capability is present in the given set
public func get(which: CapType, what: CapabilityName) -> Bool {
let bit = UInt64(1) << what.capValue
if which.contains(.effective) {
return (effectiveSet & bit) != 0
} else if which.contains(.permitted) {
return (permittedSet & bit) != 0
} else if which.contains(.inheritable) {
return (inheritableSet & bit) != 0
} else if which.contains(.bounding) {
return (boundingSet & bit) != 0
} else if which.contains(.ambient) {
return (ambientSet & bit) != 0
}
return false
}
/// Set capabilities in the given sets
public mutating func set(which: CapType, caps: [CapabilityName]) {
let mask = caps.reduce(UInt64(0)) { result, cap in
result | (UInt64(1) << cap.capValue)
}
if which.contains(.effective) {
effectiveSet |= mask
}
if which.contains(.permitted) {
permittedSet |= mask
}
if which.contains(.inheritable) {
inheritableSet |= mask
}
if which.contains(.bounding) {
boundingSet |= mask
}
if which.contains(.ambient) {
ambientSet |= mask
}
}
/// Unset capabilities from the given sets
public mutating func unset(which: CapType, caps: [CapabilityName]) {
let mask = caps.reduce(UInt64(0)) { result, cap in
result | (UInt64(1) << cap.capValue)
}
if which.contains(.effective) {
effectiveSet &= ~mask
}
if which.contains(.permitted) {
permittedSet &= ~mask
}
if which.contains(.inheritable) {
inheritableSet &= ~mask
}
if which.contains(.bounding) {
boundingSet &= ~mask
}
if which.contains(.ambient) {
ambientSet &= ~mask
}
}
/// Fill all bits of given capability types
public mutating func fill(kind: CapType) {
if kind.contains(.caps) {
effectiveSet = 0xFFFF_FFFF_FFFF_FFFF
permittedSet = 0xFFFF_FFFF_FFFF_FFFF
inheritableSet = 0
}
if kind.contains(.bounds) {
boundingSet = 0xFFFF_FFFF_FFFF_FFFF
}
if kind.contains(.ambs) {
ambientSet = 0xFFFF_FFFF_FFFF_FFFF
}
}
/// Clear all bits of given capability types
public mutating func clear(kind: CapType) {
if kind.contains(.caps) {
effectiveSet = 0
permittedSet = 0
inheritableSet = 0
}
if kind.contains(.bounds) {
boundingSet = 0
}
if kind.contains(.ambs) {
ambientSet = 0
}
}
/// Apply capabilities to current process
public func apply(kind: CapType) throws {
// Apply bounding set (requires CAP_SETPCAP)
if kind.contains(.bounds) {
try applyBoundingSet()
}
// Apply main capabilities (effective, permitted, inheritable)
if kind.contains(.caps) {
try applyMainCapabilities()
}
// Apply ambient capabilities
if kind.contains(.ambs) {
try applyAmbientCapabilities()
}
}
private func applyBoundingSet() throws {
let currentData = try getCurrentCapabilities()
let hasSetPCap = (currentData.effective1 & (1 << CapabilityName.setpcap.capValue)) != 0
if hasSetPCap {
// Get the last supported capability to avoid trying to drop unsupported ones
let lastSupported = try Self.getLastSupported()
for cap in CapabilityName.allCases {
// Skip capabilities higher than what the kernel supports
guard cap.capValue <= lastSupported.capValue else { continue }
let capBit = UInt64(1) << cap.capValue
if (boundingSet & capBit) == 0 {
let result = CZ_prctl_capbset_drop(cap.capValue)
if result != 0 && errno != EINVAL {
throw Error.prctlFailed(errno: errno, operation: "PR_CAPBSET_DROP")
}
}
}
}
}
private func applyMainCapabilities() throws {
let data = CapabilityData(
effective1: UInt32(effectiveSet & 0xFFFF_FFFF),
permitted1: UInt32(permittedSet & 0xFFFF_FFFF),
inheritable1: UInt32(inheritableSet & 0xFFFF_FFFF)
)
try setCapabilities(data: data)
}
private func applyAmbientCapabilities() throws {
// Clear all ambient capabilities first
let clearResult = CZ_prctl_cap_ambient_clear_all()
if clearResult != 0 && errno != EINVAL {
throw Error.prctlFailed(errno: errno, operation: "PR_CAP_AMBIENT_CLEAR_ALL")
}
// Get the last supported capability to avoid trying to set unsupported ones
let lastSupported = try Self.getLastSupported()
// Set each ambient capability
for cap in CapabilityName.allCases {
// Skip capabilities higher than what the kernel supports
guard cap.capValue <= lastSupported.capValue else { continue }
let capBit = UInt64(1) << cap.capValue
if (ambientSet & capBit) != 0 {
let result = CZ_prctl_cap_ambient_raise(cap.capValue)
if result != 0 && errno != EINVAL {
throw Error.prctlFailed(errno: errno, operation: "PR_CAP_AMBIENT_RAISE")
}
}
}
}
private func getCurrentCapabilities() throws -> CapabilityData {
var header = CapabilityHeader()
var data = CapabilityData()
let result = withUnsafeMutablePointer(to: &header) { headerPtr in
withUnsafeMutablePointer(to: &data) { dataPtr in
CZ_capget(headerPtr, dataPtr)
}
}
if result != 0 {
throw Error.capgetFailed(errno: errno)
}
return data
}
private func setCapabilities(data: CapabilityData) throws {
var header = CapabilityHeader()
var mutableData = data
let result = withUnsafeMutablePointer(to: &header) { headerPtr in
withUnsafeMutablePointer(to: &mutableData) { dataPtr in
CZ_capset(headerPtr, dataPtr)
}
}
if result != 0 {
throw Error.capsetFailed(errno: errno)
}
}
}
extension LinuxCapabilities {
public enum Error: Swift.Error, CustomStringConvertible {
case unsupportedCapability(name: String)
case capsetFailed(errno: Int32)
case capgetFailed(errno: Int32)
case prctlFailed(errno: Int32, operation: String)
case invalidCapabilitySet(String)
public var description: String {
switch self {
case .unsupportedCapability(let name):
return "unsupported capability: \(name)"
case .capsetFailed(let errno):
return "capset failed with errno \(errno): \(String(cString: strerror(errno)))"
case .capgetFailed(let errno):
return "capget failed with errno \(errno): \(String(cString: strerror(errno)))"
case .prctlFailed(let errno, let operation):
return "prctl(\(operation)) failed with errno \(errno): \(String(cString: strerror(errno)))"
case .invalidCapabilitySet(let message):
return "invalid capability set configuration: \(message)"
}
}
}
}
#endif
+199
View File
@@ -1237,4 +1237,203 @@ extension IntegrationSuite {
try await container.stop()
throw IntegrationError.assert(msg: "container start should have failed")
}
// MARK: - Capability Tests
func testCapabilitiesSysAdmin() async throws {
let id = "test-capabilities-sysadmin"
let bs = try await bootstrap(id)
// First test: without CAP_SYS_ADMIN (should be denied)
let bufferDenied = BufferWriter()
let containerWithoutSysAdmin = try LinuxContainer("\(id)-denied", rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.capabilities = LinuxCapabilities()
config.process.arguments = ["/bin/sh", "-c", "mount -t tmpfs tmpfs /tmp || echo 'mount failed as expected'"]
config.process.stdout = bufferDenied
config.bootLog = bs.bootLog
}
try await containerWithoutSysAdmin.create()
try await containerWithoutSysAdmin.start()
var status = try await containerWithoutSysAdmin.wait()
try await containerWithoutSysAdmin.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container should have run successfully, got exit code \(status.exitCode)")
}
guard let outputDenied = String(data: bufferDenied.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
guard outputDenied.contains("mount failed as expected") else {
throw IntegrationError.assert(msg: "expected mount failure message, got: \(outputDenied)")
}
// Second test: with CAP_SYS_ADMIN (should succeed)
let containerWithSysAdmin = try LinuxContainer("\(id)-allowed", rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.capabilities = LinuxCapabilities(capabilities: [.sysAdmin])
config.process.arguments = ["/bin/sh", "-c", "mount -t tmpfs tmpfs /tmp"]
config.bootLog = bs.bootLog
}
try await containerWithSysAdmin.create()
try await containerWithSysAdmin.start()
status = try await containerWithSysAdmin.wait()
try await containerWithSysAdmin.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container with CAP_SYS_ADMIN should mount successfully, got exit code \(status.exitCode)")
}
}
func testCapabilitiesNetAdmin() async throws {
let id = "test-capabilities-netadmin"
let bs = try await bootstrap(id)
// First test: without CAP_NET_ADMIN (should be denied)
let bufferDenied = BufferWriter()
let containerWithoutNetAdmin = try LinuxContainer("\(id)-denied", rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.capabilities = LinuxCapabilities()
config.process.arguments = ["/bin/sh", "-c", "ip link set lo down 2>/dev/null || echo 'network operation denied as expected'"]
config.process.stdout = bufferDenied
config.bootLog = bs.bootLog
}
try await containerWithoutNetAdmin.create()
try await containerWithoutNetAdmin.start()
var status = try await containerWithoutNetAdmin.wait()
try await containerWithoutNetAdmin.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container should handle network denial gracefully, got exit code \(status.exitCode)")
}
guard let outputDenied = String(data: bufferDenied.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
guard outputDenied.contains("network operation denied as expected") else {
throw IntegrationError.assert(msg: "expected network denial message, got: \(outputDenied)")
}
// Second test: with CAP_NET_ADMIN (should succeed)
let bufferAllowed = BufferWriter()
let containerWithNetAdmin = try LinuxContainer("\(id)-allowed", rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.capabilities = LinuxCapabilities(capabilities: [.netAdmin])
config.process.arguments = ["/bin/sh", "-c", "ip link set lo down && ip link set lo up"]
config.process.stdout = bufferAllowed
config.bootLog = bs.bootLog
}
try await containerWithNetAdmin.create()
try await containerWithNetAdmin.start()
status = try await containerWithNetAdmin.wait()
try await containerWithNetAdmin.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container with CAP_NET_ADMIN should perform network operations, got exit code \(status.exitCode)")
}
}
func testCapabilitiesOCIDefault() async throws {
let id = "test-capabilities-OCI-default"
let bs = try await bootstrap(id)
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
// Use default capability set
config.process.capabilities = .defaultOCICapabilities
config.process.arguments = ["/bin/sh", "-c", "echo 'Running with OCI default capabilities'"]
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: "container with OCI default capabilities should run, got exit code \(status.exitCode)")
}
}
func testCapabilitiesAllCapabilities() async throws {
let id = "test-capabilities-all"
let bs = try await bootstrap(id)
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.capabilities = .allCapabilities
config.process.arguments = ["/bin/sh", "-c", "mount -t tmpfs tmpfs /tmp && ip link set lo down"]
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: "container with all capabilities should perform all operations, got exit code \(status.exitCode)")
}
}
func testCapabilitiesFileOwnership() async throws {
let id = "test-capabilities-chown"
let bs = try await bootstrap(id)
// First test: without CAP_CHOWN
let bufferDenied = BufferWriter()
let containerWithoutChown = try LinuxContainer("\(id)-denied", rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.capabilities = LinuxCapabilities()
config.process.arguments = ["/bin/sh", "-c", "touch /tmp/testfile && chown 1000:1000 /tmp/testfile 2>/dev/null || echo 'chown denied as expected'"]
config.process.stdout = bufferDenied
config.bootLog = bs.bootLog
}
try await containerWithoutChown.create()
try await containerWithoutChown.start()
var status = try await containerWithoutChown.wait()
try await containerWithoutChown.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container should handle chown denial gracefully, got exit code \(status.exitCode)")
}
guard let outputDenied = String(data: bufferDenied.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
guard outputDenied.contains("chown denied as expected") else {
throw IntegrationError.assert(msg: "expected chown denial message, got: \(outputDenied)")
}
// Second test: with CAP_CHOWN
let bufferAllowed = BufferWriter()
let containerWithChown = try LinuxContainer("\(id)-allowed", rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.capabilities = LinuxCapabilities(capabilities: [.chown])
config.process.arguments = ["/bin/sh", "-c", "touch /tmp/testfile && chown 1000:1000 /tmp/testfile"]
config.process.stdout = bufferAllowed
config.bootLog = bs.bootLog
}
try await containerWithChown.create()
try await containerWithChown.start()
status = try await containerWithChown.wait()
try await containerWithChown.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container with CAP_CHOWN should succeed, got exit code \(status.exitCode)")
}
}
}
+5
View File
@@ -300,6 +300,11 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("process delete idempotency", testProcessDeleteIdempotency),
Test("multiple execs without delete", testMultipleExecsWithoutDelete),
Test("container bootlog using filehandle", testBootLogFileHandle),
Test("container capabilities sys admin", testCapabilitiesSysAdmin),
Test("container capabilities net admin", testCapabilitiesNetAdmin),
Test("container capabilities OCI default", testCapabilitiesOCIDefault),
Test("container capabilities all capabilities", testCapabilitiesAllCapabilities),
Test("container capabilities file ownership", testCapabilitiesFileOwnership),
// Pods
Test("pod single container", testPodSingleContainer),
+1
View File
@@ -100,6 +100,7 @@ extension Application {
config.process.setTerminalIO(terminal: current)
config.process.arguments = arguments
config.process.workingDirectory = cwd
config.process.capabilities = .allCapabilities
for mount in self.mounts {
let paths = mount.split(separator: ":")
+7
View File
@@ -16,6 +16,7 @@
import ArgumentParser
import ContainerizationOCI
import ContainerizationOS
import Foundation
import LCShim
import Logging
@@ -133,12 +134,18 @@ struct ExecCommand: ParsableCommand {
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)
try App.exec(process: process)
} else { // parent process
// Send our child's pid to our parent before we exit.
+7
View File
@@ -17,6 +17,7 @@
import ArgumentParser
import Cgroup
import ContainerizationOCI
import ContainerizationOS
import Foundation
import LCShim
import Logging
@@ -141,12 +142,18 @@ struct RunCommand: ParsableCommand {
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)
// Finally execve the container process.
try App.exec(process: process, currentEnv: process.env)
}
+45
View File
@@ -171,6 +171,51 @@ extension App {
}
}
static func prepareCapabilities(capabilities: ContainerizationOCI.LinuxCapabilities) throws -> ContainerizationOS.LinuxCapabilities? {
// Create capabilities instance from OCI config
var caps = ContainerizationOS.LinuxCapabilities()
caps.set(which: [.effective], caps: (capabilities.effective ?? []).compactMap { CapabilityName(rawValue: $0) })
caps.set(which: [.permitted], caps: (capabilities.permitted ?? []).compactMap { CapabilityName(rawValue: $0) })
caps.set(which: [.inheritable], caps: (capabilities.inheritable ?? []).compactMap { CapabilityName(rawValue: $0) })
caps.set(which: [.bounding], caps: (capabilities.bounding ?? []).compactMap { CapabilityName(rawValue: $0) })
caps.set(which: [.ambient], caps: (capabilities.ambient ?? []).compactMap { CapabilityName(rawValue: $0) })
// Apply bounding set BEFORE user change (drop capabilities early)
do {
try caps.apply(kind: .bounds)
} catch {
throw App.Failure(message: "failed to apply bounding set capabilities: \(error)")
}
// Set keep caps to preserve capabilities across setuid()
do {
try LinuxCapabilities.setKeepCaps()
} catch {
throw App.Failure(message: "failed to set keep caps: \(error)")
}
return caps
}
static func finishCapabilities(_ caps: ContainerizationOS.LinuxCapabilities?) throws {
guard let caps = caps else { return }
do {
try LinuxCapabilities.clearKeepCaps()
} catch {
throw App.Failure(message: "failed to clear keep caps: \(error)")
}
do {
try caps.apply(kind: [.caps])
} catch {
throw App.Failure(message: "failed to apply final capabilities: \(error)")
}
try? caps.apply(kind: [.ambs])
}
static func Errno(stage: String, info: String = "") -> ContainerizationError {
let posix = POSIXError(.init(rawValue: errno)!, userInfo: ["stage": stage])
return ContainerizationError(.internalError, message: "\(info) \(String(describing: posix))")