Skip to content
Closed
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
18 changes: 18 additions & 0 deletions Sources/ContainerResource/Container/ProcessConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ public struct ProcessConfiguration: Sendable, Codable {
return name
}
}

/// Returns true when the user specification explicitly resolves to a non-root user.
/// Unknown named users are treated as non-root for safer default behavior.
public var isExplicitlyNonRoot: Bool {
switch self {
case .id(let uid, _):
return uid != 0
case .raw(let userString):
let primaryToken = userString
.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
.first
.map(String.init) ?? ""
if let uid = UInt32(primaryToken) {
return uid != 0
}
return primaryToken.lowercased() != "root"
}
}
}

public init(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,10 @@ public actor SandboxService {
username: ""
)
}

if process.user.isExplicitlyNonRoot {
czConfig.process.capabilities = Containerization.LinuxCapabilities()
}
}

private nonisolated func configureProcessConfig(config: ProcessConfiguration, stdio: [FileHandle?], containerConfig: ContainerConfiguration)
Expand Down Expand Up @@ -1003,6 +1007,10 @@ public actor SandboxService {
)
}

if config.user.isExplicitlyNonRoot {
proc.capabilities = Containerization.LinuxCapabilities()
}

return proc
}

Expand Down
35 changes: 35 additions & 0 deletions Tests/CLITests/Subcommands/Containers/TestCLIExec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,39 @@ class TestCLIExecCommand: CLITest {
}
}
}

@Test func testExecUserRespectsFilePermissions() throws {
let name = getTestName()
try doLongRun(name: name)
defer {
try? doStop(name: name)
}

let (_, idOutput, idError, idStatus) = try run(arguments: [
"exec",
"-u",
"nobody",
name,
"id",
])
try #require(idStatus == 0, "id should run as nobody: stderr=\(idError)")
#expect(idOutput.contains("uid=65534"), "expected uid 65534 for nobody, got: \(idOutput)")

let (_, output, error, status) = try run(arguments: [
"exec",
"-u",
"nobody",
name,
"cat",
"/etc/shadow",
])

#expect(status != 0, "expected permission failure, got success with output: \(output)")

let combined = "\(output)\n\(error)"
#expect(
combined.localizedCaseInsensitiveContains("permission denied"),
"expected permission denied error, got: \(combined)"
)
}
}