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
7 changes: 7 additions & 0 deletions Sources/Containerization/LinuxProcessConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ public struct LinuxProcessConfiguration: Sendable {
public var user: ContainerizationOCI.User = .init()
/// The rlimits for the container process.
public var rlimits: [LinuxRLimit] = []
/// Whether to set the no_new_privileges bit on the container process. When true, the
/// process and its children cannot gain additional privileges via setuid/setgid binaries
/// or file capabilities.
public var noNewPrivileges: Bool = false
/// 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 @@ -393,6 +397,7 @@ public struct LinuxProcessConfiguration: Sendable {
workingDirectory: String = "/",
user: ContainerizationOCI.User = .init(),
rlimits: [LinuxRLimit] = [],
noNewPrivileges: Bool = false,
capabilities: LinuxCapabilities = .allCapabilities,
terminal: Bool = false,
stdin: ReaderStream? = nil,
Expand All @@ -404,6 +409,7 @@ public struct LinuxProcessConfiguration: Sendable {
self.workingDirectory = workingDirectory
self.user = user
self.rlimits = rlimits
self.noNewPrivileges = noNewPrivileges
self.capabilities = capabilities
self.terminal = terminal
self.stdin = stdin
Expand Down Expand Up @@ -437,6 +443,7 @@ public struct LinuxProcessConfiguration: Sendable {
args: self.arguments,
cwd: self.workingDirectory,
env: self.environmentVariables,
noNewPrivileges: self.noNewPrivileges,
capabilities: self.capabilities.toOCI(),
user: self.user,
rlimits: self.rlimits.map { $0.toOCI() },
Expand Down
110 changes: 110 additions & 0 deletions Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4153,4 +4153,114 @@ extension IntegrationSuite {
throw error
}
}

func testNoNewPrivileges() async throws {
let id = "test-no-new-privileges"

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 = ["cat", "/proc/self/status"]
config.process.noNewPrivileges = true
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")
}

// /proc/self/status contains "NoNewPrivs:\t1" when the bit is set
guard output.contains("NoNewPrivs:\t1") else {
throw IntegrationError.assert(msg: "expected NoNewPrivs to be 1, got: \(output)")
}
}

func testNoNewPrivilegesDisabled() async throws {
let id = "test-no-new-privileges-disabled"

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 = ["cat", "/proc/self/status"]
// noNewPrivileges defaults to false
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")
}

// When noNewPrivileges is not set, NoNewPrivs should be 0
guard output.contains("NoNewPrivs:\t0") else {
throw IntegrationError.assert(msg: "expected NoNewPrivs to be 0, got: \(output)")
}
}

func testNoNewPrivilegesExec() async throws {
let id = "test-no-new-privileges-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 noNewPrivileges set
let buffer = BufferWriter()
let exec = try await container.exec("nnp-exec") { config in
config.arguments = ["cat", "/proc/self/status"]
config.noNewPrivileges = true
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) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}

guard output.contains("NoNewPrivs:\t1") else {
throw IntegrationError.assert(msg: "expected NoNewPrivs to be 1 in exec, got: \(output)")
}

try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
}
3 changes: 3 additions & 0 deletions Sources/Integration/Suite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("container useInit with stdin", testUseInitWithStdin),
Test("container sysctl", testSysctl),
Test("container sysctl multiple", testSysctlMultiple),
Test("container noNewPrivileges", testNoNewPrivileges),
Test("container noNewPrivileges disabled", testNoNewPrivilegesDisabled),
Test("container noNewPrivileges exec", testNoNewPrivilegesExec),

// Pods
Test("pod single container", testPodSingleContainer),
Expand Down
2 changes: 2 additions & 0 deletions vminitd/Sources/LCShim/include/syscall.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ int CZ_pidfd_open(pid_t pid, unsigned int flags);

int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags);

int CZ_prctl_set_no_new_privs();

#endif
4 changes: 4 additions & 0 deletions vminitd/Sources/LCShim/syscall.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags) {
// Musl doesn't have pidfd_getfd.
return syscall(SYS_pidfd_getfd, pidfd, targetfd, flags);
}

int CZ_prctl_set_no_new_privs() {
return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
}
3 changes: 3 additions & 0 deletions vminitd/Sources/vmexec/ExecCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ struct ExecCommand: ParsableCommand {
// Finish capabilities (after user change)
try App.finishCapabilities(preparedCaps)

// Set no_new_privs if requested by the OCI spec.
try App.setNoNewPrivileges(process: process)

try App.exec(process: process, currentEnv: process.env)
} else { // parent process
// Send our child's pid to our parent before we exit.
Expand Down
3 changes: 3 additions & 0 deletions vminitd/Sources/vmexec/RunCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ struct RunCommand: ParsableCommand {
// Finish capabilities (after user change)
try App.finishCapabilities(preparedCaps)

// Set no_new_privs if requested by the OCI spec.
try App.setNoNewPrivileges(process: process)

// Finally execve the container process.
try App.exec(process: process, currentEnv: process.env)
}
Expand Down
7 changes: 7 additions & 0 deletions vminitd/Sources/vmexec/vmexec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ extension App {
try? caps.apply(kind: [.ambs])
}

static func setNoNewPrivileges(process: ContainerizationOCI.Process) throws {
guard process.noNewPrivileges else { return }
guard CZ_prctl_set_no_new_privs() == 0 else {
throw App.Errno(stage: "prctl(PR_SET_NO_NEW_PRIVS)")
}
}

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))")
Expand Down
Loading