mirror of
https://github.com/apple/container.git
synced 2026-07-13 13:07:06 +00:00
69a2505aeb
- Part of #1833.
435 lines
17 KiB
Swift
435 lines
17 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2026 Apple Inc. and the container 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 Foundation
|
|
import Testing
|
|
|
|
@Suite
|
|
struct TestCLIRunCapabilities {
|
|
private let alpine = ContainerFixture.warmupImages[0]
|
|
|
|
// MARK: - Invalid capability names
|
|
|
|
@Test func testCapDropInvalid() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let result = try f.run(["run", "--rm", "--cap-drop=CHWOWZERS", image, "ls"])
|
|
#expect(result.status != 0)
|
|
#expect(result.error.contains("CHWOWZERS") || result.error.contains("invalid"))
|
|
}
|
|
}
|
|
|
|
@Test func testCapAddInvalid() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let result = try f.run(["run", "--rm", "--cap-add=CHWOWZERS", image, "ls"])
|
|
#expect(result.status != 0)
|
|
#expect(result.error.contains("CHWOWZERS") || result.error.contains("invalid"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Config stored correctly via inspect
|
|
|
|
@Test func testCapAddStored() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-add", "NET_ADMIN"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let inspect = try f.inspectContainer(c)
|
|
#expect(inspect.configuration.capAdd.contains("CAP_NET_ADMIN"))
|
|
#expect(inspect.configuration.capDrop.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test func testCapDropStored() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-drop", "MKNOD"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let inspect = try f.inspectContainer(c)
|
|
#expect(inspect.configuration.capDrop.contains("CAP_MKNOD"))
|
|
#expect(inspect.configuration.capAdd.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test func testCapAddDropALLStored() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(
|
|
name: c, image: image,
|
|
args: ["--cap-drop", "ALL", "--cap-add", "SETGID", "--cap-add", "NET_RAW"],
|
|
autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let inspect = try f.inspectContainer(c)
|
|
#expect(inspect.configuration.capDrop.contains("ALL"))
|
|
#expect(inspect.configuration.capAdd.contains("CAP_SETGID"))
|
|
#expect(inspect.configuration.capAdd.contains("CAP_NET_RAW"))
|
|
}
|
|
}
|
|
|
|
@Test func testCapAddALLStored() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-add", "ALL"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let inspect = try f.inspectContainer(c)
|
|
#expect(inspect.configuration.capAdd.contains("ALL"))
|
|
}
|
|
}
|
|
|
|
@Test func testCapDropLowerCase() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-drop", "mknod"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let inspect = try f.inspectContainer(c)
|
|
#expect(inspect.configuration.capDrop.contains("CAP_MKNOD"))
|
|
}
|
|
}
|
|
|
|
// MARK: - In-container capability verification
|
|
|
|
@Test func testCapDropMknodCannotMknod() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-drop", "MKNOD"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let result = try f.run(["exec", c, "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok"])
|
|
#expect(result.output.trimmingCharacters(in: .whitespacesAndNewlines) != "ok")
|
|
#expect(result.status != 0)
|
|
}
|
|
}
|
|
|
|
@Test func testCapDropMknodLowerCaseCannotMknod() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-drop", "mknod"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let result = try f.run(["exec", c, "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok"])
|
|
#expect(result.output.trimmingCharacters(in: .whitespacesAndNewlines) != "ok")
|
|
#expect(result.status != 0)
|
|
}
|
|
}
|
|
|
|
@Test func testCapDropALLCannotMknod() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(
|
|
name: c, image: image,
|
|
args: ["--cap-drop", "ALL", "--cap-add", "SETGID"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let result = try f.run(["exec", c, "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok"])
|
|
#expect(result.output.trimmingCharacters(in: .whitespacesAndNewlines) != "ok")
|
|
#expect(result.status != 0)
|
|
}
|
|
}
|
|
|
|
@Test func testCapDropALLAddMknodCanMknod() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(
|
|
name: c, image: image,
|
|
args: ["--cap-drop", "ALL", "--cap-add", "MKNOD", "--cap-add", "SETGID"],
|
|
autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let output = try f.doExec(c, cmd: ["sh", "-c", "mknod /tmp/sda b 8 0 && echo ok"])
|
|
#expect(output.trimmingCharacters(in: .whitespacesAndNewlines) == "ok")
|
|
}
|
|
}
|
|
|
|
@Test func testCapAddALLCanDownInterface() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-add", "ALL"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let output = try f.doExec(c, cmd: ["sh", "-c", "ip link set lo down && echo ok"])
|
|
#expect(output.trimmingCharacters(in: .whitespacesAndNewlines) == "ok")
|
|
}
|
|
}
|
|
|
|
@Test func testCapAddALLDropNetAdminCannotDownInterface() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(
|
|
name: c, image: image,
|
|
args: ["--cap-add", "ALL", "--cap-drop", "NET_ADMIN"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let result = try f.run(["exec", c, "sh", "-c", "ip link set lo down && echo ok"])
|
|
#expect(result.output.trimmingCharacters(in: .whitespacesAndNewlines) != "ok")
|
|
#expect(result.status != 0)
|
|
}
|
|
}
|
|
|
|
@Test func testCapAddNetAdminCanDownInterface() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-add", "NET_ADMIN"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let output = try f.doExec(c, cmd: ["sh", "-c", "ip link set lo down && echo ok"])
|
|
#expect(output.trimmingCharacters(in: .whitespacesAndNewlines) == "ok")
|
|
}
|
|
}
|
|
|
|
// MARK: - Default capability behavior
|
|
|
|
@Test func testDefaultCapChown() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
_ = try f.doExec(c, cmd: ["chown", "100", "/tmp"])
|
|
}
|
|
}
|
|
|
|
@Test func testNonRootUserCannotReadShadow() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
_ = try f.doExec(c, cmd: ["cat", "/etc/shadow"])
|
|
let result = try f.run(["exec", "-u", "nobody", c, "cat", "/etc/shadow"])
|
|
#expect(result.status != 0, "non-root user should not be able to read /etc/shadow")
|
|
}
|
|
}
|
|
|
|
@Test func testCapDropChown() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-drop", "chown"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let result = try f.run(["exec", c, "chown", "100", "/tmp"])
|
|
#expect(result.status != 0)
|
|
}
|
|
}
|
|
|
|
@Test func testDefaultCapFowner() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
_ = try f.doExec(c, cmd: ["chmod", "777", "/etc/passwd"])
|
|
}
|
|
}
|
|
|
|
// MARK: - Capability bitmask verification via /proc
|
|
|
|
@Test func testCapDropALLShowsZeroCaps() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(
|
|
name: c, image: image,
|
|
args: ["--cap-drop", "ALL", "--cap-add", "SETUID", "--cap-add", "SETGID"],
|
|
autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let output = try f.doExec(c, cmd: ["cat", "/proc/self/status"])
|
|
let capEff = output.components(separatedBy: "\n").first { $0.hasPrefix("CapEff:") }
|
|
try #require(capEff != nil)
|
|
let value = capEff!.replacingOccurrences(of: "CapEff:", with: "").trimmingCharacters(in: .whitespaces)
|
|
#expect(value != "0000000000000000", "expected non-zero CapEff with SETUID+SETGID")
|
|
#expect(value != "000001ffffffffff", "expected restricted caps, not full set")
|
|
}
|
|
}
|
|
|
|
@Test func testNoCapFlagsUsesDefaultCaps() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let output = try f.doExec(c, cmd: ["cat", "/proc/self/status"])
|
|
let capEff = output.components(separatedBy: "\n").first { $0.hasPrefix("CapEff:") }
|
|
try #require(capEff != nil)
|
|
let value = capEff!.replacingOccurrences(of: "CapEff:", with: "").trimmingCharacters(in: .whitespaces)
|
|
#expect(value != "0000000000000000")
|
|
}
|
|
}
|
|
|
|
@Test func testCapAddALLShowsFullCaps() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-add", "ALL"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let output = try f.doExec(c, cmd: ["cat", "/proc/self/status"])
|
|
let capEff = output.components(separatedBy: "\n").first { $0.hasPrefix("CapEff:") }
|
|
try #require(capEff != nil)
|
|
let value = capEff!.replacingOccurrences(of: "CapEff:", with: "").trimmingCharacters(in: .whitespaces)
|
|
#expect(value != "0000000000000000")
|
|
}
|
|
}
|
|
|
|
@Test func testCapDropALLOnlyShowsZeroEffective() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(name: c, image: image, args: ["--cap-drop", "ALL"], autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let output = try f.doExec(c, cmd: ["cat", "/proc/self/status"])
|
|
let capEff = output.components(separatedBy: "\n").first { $0.hasPrefix("CapEff:") }
|
|
try #require(capEff != nil)
|
|
let value = capEff!.replacingOccurrences(of: "CapEff:", with: "").trimmingCharacters(in: .whitespaces)
|
|
#expect(value == "0000000000000000", "expected zero CapEff when ALL caps dropped, got \(value)")
|
|
}
|
|
}
|
|
|
|
@Test func testMultipleCapAddDrop() async throws {
|
|
try await ContainerFixture.with { f in
|
|
let image = try f.copyWarmupImage(alpine)
|
|
let c = "\(f.testID)-c"
|
|
try f.doLongRun(
|
|
name: c, image: image,
|
|
args: [
|
|
"--cap-add", "SYS_ADMIN", "--cap-add", "NET_RAW",
|
|
"--cap-drop", "MKNOD", "--cap-drop", "CHOWN",
|
|
],
|
|
autoRemove: false)
|
|
try await f.waitForContainerRunning(c)
|
|
f.addCleanup {
|
|
try? f.doStop(c)
|
|
try? f.doRemove(c)
|
|
}
|
|
|
|
let inspect = try f.inspectContainer(c)
|
|
#expect(inspect.configuration.capAdd.count == 2)
|
|
#expect(inspect.configuration.capDrop.count == 2)
|
|
#expect(inspect.configuration.capAdd.contains("CAP_SYS_ADMIN"))
|
|
#expect(inspect.configuration.capAdd.contains("CAP_NET_RAW"))
|
|
#expect(inspect.configuration.capDrop.contains("CAP_MKNOD"))
|
|
#expect(inspect.configuration.capDrop.contains("CAP_CHOWN"))
|
|
|
|
let result = try f.run(["exec", c, "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok"])
|
|
#expect(result.output.trimmingCharacters(in: .whitespacesAndNewlines) != "ok")
|
|
}
|
|
}
|
|
}
|