Skip to content

Commit 26d80e9

Browse files
committed
LinuxContainer/LinuxPod: Add support for NoNewPrivileges
Basically just a direct mapping to the prctl/OCI spec field.
1 parent 735dad6 commit 26d80e9

8 files changed

Lines changed: 139 additions & 0 deletions

File tree

Sources/Containerization/LinuxProcessConfiguration.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,10 @@ public struct LinuxProcessConfiguration: Sendable {
378378
/// behavior and are planning to use a terminal for stdin/out/err on the client side,
379379
/// this should likely be set to true.
380380
public var terminal: Bool = false
381+
/// Whether to set the no_new_privileges bit on the container process. When true, the
382+
/// process and its children cannot gain additional privileges via setuid/setgid binaries
383+
/// or file capabilities.
384+
public var noNewPrivileges: Bool = false
381385
/// The stdin for the process.
382386
public var stdin: ReaderStream?
383387
/// The stdout for the process.
@@ -395,6 +399,7 @@ public struct LinuxProcessConfiguration: Sendable {
395399
rlimits: [LinuxRLimit] = [],
396400
capabilities: LinuxCapabilities = .allCapabilities,
397401
terminal: Bool = false,
402+
noNewPrivileges: Bool = false,
398403
stdin: ReaderStream? = nil,
399404
stdout: Writer? = nil,
400405
stderr: Writer? = nil
@@ -406,6 +411,7 @@ public struct LinuxProcessConfiguration: Sendable {
406411
self.rlimits = rlimits
407412
self.capabilities = capabilities
408413
self.terminal = terminal
414+
self.noNewPrivileges = noNewPrivileges
409415
self.stdin = stdin
410416
self.stdout = stdout
411417
self.stderr = stderr
@@ -437,6 +443,7 @@ public struct LinuxProcessConfiguration: Sendable {
437443
args: self.arguments,
438444
cwd: self.workingDirectory,
439445
env: self.environmentVariables,
446+
noNewPrivileges: self.noNewPrivileges,
440447
capabilities: self.capabilities.toOCI(),
441448
user: self.user,
442449
rlimits: self.rlimits.map { $0.toOCI() },

Sources/Integration/ContainerTests.swift

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4076,4 +4076,114 @@ extension IntegrationSuite {
40764076
throw error
40774077
}
40784078
}
4079+
4080+
func testNoNewPrivileges() async throws {
4081+
let id = "test-no-new-privileges"
4082+
4083+
let bs = try await bootstrap(id)
4084+
let buffer = BufferWriter()
4085+
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
4086+
config.process.arguments = ["cat", "/proc/self/status"]
4087+
config.process.noNewPrivileges = true
4088+
config.process.stdout = buffer
4089+
config.bootLog = bs.bootLog
4090+
}
4091+
4092+
try await container.create()
4093+
try await container.start()
4094+
4095+
let status = try await container.wait()
4096+
try await container.stop()
4097+
4098+
guard status.exitCode == 0 else {
4099+
throw IntegrationError.assert(msg: "process status \(status) != 0")
4100+
}
4101+
4102+
guard let output = String(data: buffer.data, encoding: .utf8) else {
4103+
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
4104+
}
4105+
4106+
// /proc/self/status contains "NoNewPrivs:\t1" when the bit is set
4107+
guard output.contains("NoNewPrivs:\t1") else {
4108+
throw IntegrationError.assert(msg: "expected NoNewPrivs to be 1, got: \(output)")
4109+
}
4110+
}
4111+
4112+
func testNoNewPrivilegesDisabled() async throws {
4113+
let id = "test-no-new-privileges-disabled"
4114+
4115+
let bs = try await bootstrap(id)
4116+
let buffer = BufferWriter()
4117+
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
4118+
config.process.arguments = ["cat", "/proc/self/status"]
4119+
// noNewPrivileges defaults to false
4120+
config.process.stdout = buffer
4121+
config.bootLog = bs.bootLog
4122+
}
4123+
4124+
try await container.create()
4125+
try await container.start()
4126+
4127+
let status = try await container.wait()
4128+
try await container.stop()
4129+
4130+
guard status.exitCode == 0 else {
4131+
throw IntegrationError.assert(msg: "process status \(status) != 0")
4132+
}
4133+
4134+
guard let output = String(data: buffer.data, encoding: .utf8) else {
4135+
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
4136+
}
4137+
4138+
// When noNewPrivileges is not set, NoNewPrivs should be 0
4139+
guard output.contains("NoNewPrivs:\t0") else {
4140+
throw IntegrationError.assert(msg: "expected NoNewPrivs to be 0, got: \(output)")
4141+
}
4142+
}
4143+
4144+
func testNoNewPrivilegesExec() async throws {
4145+
let id = "test-no-new-privileges-exec"
4146+
4147+
let bs = try await bootstrap(id)
4148+
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
4149+
config.process.arguments = ["sleep", "100"]
4150+
config.bootLog = bs.bootLog
4151+
}
4152+
4153+
do {
4154+
try await container.create()
4155+
try await container.start()
4156+
4157+
// Exec a process with noNewPrivileges set
4158+
let buffer = BufferWriter()
4159+
let exec = try await container.exec("nnp-exec") { config in
4160+
config.arguments = ["cat", "/proc/self/status"]
4161+
config.noNewPrivileges = true
4162+
config.stdout = buffer
4163+
}
4164+
4165+
try await exec.start()
4166+
let status = try await exec.wait()
4167+
try await exec.delete()
4168+
4169+
guard status.exitCode == 0 else {
4170+
throw IntegrationError.assert(msg: "exec status \(status) != 0")
4171+
}
4172+
4173+
guard let output = String(data: buffer.data, encoding: .utf8) else {
4174+
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
4175+
}
4176+
4177+
guard output.contains("NoNewPrivs:\t1") else {
4178+
throw IntegrationError.assert(msg: "expected NoNewPrivs to be 1 in exec, got: \(output)")
4179+
}
4180+
4181+
try await container.kill(SIGKILL)
4182+
try await container.wait()
4183+
try await container.stop()
4184+
} catch {
4185+
try? await container.stop()
4186+
throw error
4187+
}
4188+
}
40794189
}

Sources/Integration/Suite.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ struct IntegrationSuite: AsyncParsableCommand {
369369
Test("container useInit zombie reaping", testUseInitZombieReaping),
370370
Test("container useInit with terminal", testUseInitWithTerminal),
371371
Test("container useInit with stdin", testUseInitWithStdin),
372+
Test("container noNewPrivileges", testNoNewPrivileges),
373+
Test("container noNewPrivileges disabled", testNoNewPrivilegesDisabled),
374+
Test("container noNewPrivileges exec", testNoNewPrivilegesExec),
372375

373376
// Pods
374377
Test("pod single container", testPodSingleContainer),

vminitd/Sources/LCShim/include/syscall.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,6 @@ int CZ_pidfd_open(pid_t pid, unsigned int flags);
3535

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

38+
int CZ_prctl_set_no_new_privs();
39+
3840
#endif

vminitd/Sources/LCShim/syscall.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags) {
3535
// Musl doesn't have pidfd_getfd.
3636
return syscall(SYS_pidfd_getfd, pidfd, targetfd, flags);
3737
}
38+
39+
int CZ_prctl_set_no_new_privs() {
40+
return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
41+
}

vminitd/Sources/vmexec/ExecCommand.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ struct ExecCommand: ParsableCommand {
145145
// Finish capabilities (after user change)
146146
try App.finishCapabilities(preparedCaps)
147147

148+
// Set no_new_privs if requested by the OCI spec.
149+
try App.setNoNewPrivileges(process: process)
150+
148151
try App.exec(process: process, currentEnv: process.env)
149152
} else { // parent process
150153
// Send our child's pid to our parent before we exit.

vminitd/Sources/vmexec/RunCommand.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ struct RunCommand: ParsableCommand {
196196
// Finish capabilities (after user change)
197197
try App.finishCapabilities(preparedCaps)
198198

199+
// Set no_new_privs if requested by the OCI spec.
200+
try App.setNoNewPrivileges(process: process)
201+
199202
// Finally execve the container process.
200203
try App.exec(process: process, currentEnv: process.env)
201204
}

vminitd/Sources/vmexec/vmexec.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ extension App {
233233
try? caps.apply(kind: [.ambs])
234234
}
235235

236+
static func setNoNewPrivileges(process: ContainerizationOCI.Process) throws {
237+
guard process.noNewPrivileges else { return }
238+
guard CZ_prctl_set_no_new_privs() == 0 else {
239+
throw App.Errno(stage: "prctl(PR_SET_NO_NEW_PRIVS)")
240+
}
241+
}
242+
236243
static func Errno(stage: String, info: String = "") -> ContainerizationError {
237244
let posix = POSIXError(.init(rawValue: errno)!, userInfo: ["stage": stage])
238245
return ContainerizationError(.internalError, message: "\(info) \(String(describing: posix))")

0 commit comments

Comments
 (0)