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
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ LIBARCHIVE_UPSTREAM_REPO := https://github.com/libarchive/libarchive
LIBARCHIVE_UPSTREAM_VERSION := v3.7.7
LIBARCHIVE_LOCAL_DIR := workdir/libarchive

KATA_BINARY_PACKAGE := https://github.com/kata-containers/kata-containers/releases/download/3.17.0/kata-static-3.17.0-arm64.tar.xz
KATA_BINARY_PACKAGE := https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst
CLOUD_HYPERVISOR_URL := https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v52.0/cloud-hypervisor-static-aarch64
# SHA256 of the v52.0 aarch64 static binary (verified locally from the
# upstream release artifact). Bump alongside CLOUD_HYPERVISOR_URL.
Expand Down Expand Up @@ -410,11 +410,11 @@ integration:
.PHONY: fetch-default-kernel
fetch-default-kernel:
@mkdir -p .local/ bin/
ifeq (,$(wildcard .local/kata.tar.gz))
@curl -SsL -o .local/kata.tar.gz ${KATA_BINARY_PACKAGE}
ifeq (,$(wildcard .local/kata.tar))
@curl -SsL -o .local/kata.tar ${KATA_BINARY_PACKAGE}
endif
ifeq (,$(wildcard .local/vmlinux-$(KERNEL_ARCH)))
@tar -zxf .local/kata.tar.gz -C .local/ --strip-components=1
@tar -xf .local/kata.tar -C .local/ --strip-components=1
@cp -L .local/opt/kata/share/kata-containers/vmlinux.container .local/vmlinux-$(KERNEL_ARCH)
endif
ifeq (,$(wildcard bin/vmlinux-$(KERNEL_ARCH)))
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ After building, run basic and integration tests:
make test integration
```

Tests that only apply to Linux are compiled out on macOS, so `make test` passes
without running them. Run those with:

```bash
make linux-test
```

which runs the same tests inside the Linux dev container, as the Linux build
workflow does.

A kernel is required to run integration tests.
If you do not have a kernel locally, a default kernel can be fetched using the `make fetch-default-kernel` target.

Expand Down
33 changes: 23 additions & 10 deletions Sources/ContainerizationOS/Mount/Mount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,29 @@ extension Mount {
"sync": .init(false, MS_SYNCHRONOUS),
]

/// Mount propagation options, which the kernel takes in a call of their
/// own rather than alongside the mount whose propagation they set.
/// https://github.com/opencontainers/runc/blob/main/libcontainer/specconv/spec_linux.go
internal static let propagationDictionary: [String: Int32] = [
"private": Int32(MS_PRIVATE),
"rprivate": Int32(MS_PRIVATE) | Int32(MS_REC),
"shared": Int32(MS_SHARED),
"rshared": Int32(MS_SHARED) | Int32(MS_REC),
"slave": Int32(MS_SLAVE),
"rslave": Int32(MS_SLAVE) | Int32(MS_REC),
"unbindable": Int32(MS_UNBINDABLE),
"runbindable": Int32(MS_UNBINDABLE) | Int32(MS_REC),
]

internal struct MountOptions {
var flags: Int32
var data: [String]
var propagation: [Int32]

public init(_ flags: Int32 = 0, data: [String] = []) {
public init(_ flags: Int32 = 0, data: [String] = [], propagation: [Int32] = []) {
self.flags = flags
self.data = data
self.propagation = propagation
}
}

Expand Down Expand Up @@ -304,10 +320,7 @@ extension Mount {
throw Error.validation("data string exceeds page size (\(dataString.count) > \(pageSize))")
}

let propagationTypes: Int32 = Int32(MS_SHARED) | Int32(MS_PRIVATE) | Int32(MS_SLAVE) | Int32(MS_UNBINDABLE)

// Ensure propagation type change flags aren't included in other calls.
let originalFlags = opts.flags & ~(propagationTypes)
let originalFlags = opts.flags

// When targetResolved is true, the target path has already been securely
// resolved and the mount point created by secureResolveInRoot. Skip
Expand Down Expand Up @@ -352,10 +365,8 @@ extension Mount {
}
}

if opts.flags & propagationTypes != 0 {
// Change the propagation type.
let pflags = propagationTypes | Int32(MS_REC) | Int32(MS_SILENT)
guard _mount("", target, "", UInt(opts.flags & pflags), "") == 0 else {
for propagation in opts.propagation {
guard _mount("", target, "", UInt(propagation | Int32(MS_SILENT)), "") == 0 else {
throw Error.errno(errno, "failed propagation change mount")
}
}
Expand All @@ -379,7 +390,9 @@ extension Mount {
private func parseMountOptions() -> MountOptions {
var mountOpts = MountOptions()
for option in self.options {
if let entry = Self.flagsDictionary[option], entry.flag != 0 {
if let propagation = Self.propagationDictionary[option] {
mountOpts.propagation.append(propagation)
} else if let entry = Self.flagsDictionary[option], entry.flag != 0 {
if entry.clear {
mountOpts.flags &= ~Int32(entry.flag)
} else {
Expand Down
33 changes: 33 additions & 0 deletions Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,39 @@ extension IntegrationSuite {
}
}

/// A mount's propagation is set by a mount call of its own, so asking for
/// it has to survive the option parsing rather than land in the mount data.
func testContainerMountPropagation() async throws {
let id = "test-container-mount-propagation"
let bs = try await bootstrap(id)

let buffer = BufferWriter()
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.mounts.append(
.any(
type: "tmpfs", source: "tmpfs", destination: "/shared-mount",
options: ["rw", "nosuid", "nodev", "rshared"]))
config.process.arguments = [
"/bin/sh", "-c", "grep ' /shared-mount ' /proc/self/mountinfo",
]
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: "mount is missing from mountinfo")
}
// Only a mount the kernel was told to share carries a peer group.
let info = String(data: buffer.data, encoding: .utf8) ?? ""
guard info.contains("shared:") else {
throw IntegrationError.assert(msg: "mount was not made shared: '\(info)'")
}
}

func testProcessEchoHi() async throws {
let id = "test-process-echo-hi"
let bs = try await bootstrap(id)
Expand Down
9 changes: 7 additions & 2 deletions Sources/Integration/Suite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,16 @@ struct IntegrationSuite: AsyncParsableCommand {
// a ~2GB rootfs and a ~512MB initfs, so without reaping the dev
// container fills its CoW layer in ~10 tests.
if self.maxConcurrency == 1 {
let preserve = fsPath.absolutePath()
// contentsOfDirectory reports paths under /private, while testDir is
// built from FileManager's /var view of the same directory, so both
// sides are resolved before comparing.
let preserve = fsPath.resolvingSymlinksInPathWithPrivate().absolutePath()
if let entries = try? FileManager.default.contentsOfDirectory(
at: Self.testDir,
includingPropertiesForKeys: nil
) {
for url in entries where url.absolutePath() != preserve {
for url in entries
where url.resolvingSymlinksInPathWithPrivate().absolutePath() != preserve {
try? FileManager.default.removeItem(at: url)
}
}
Expand Down Expand Up @@ -422,6 +426,7 @@ struct IntegrationSuite: AsyncParsableCommand {
// Process basics
Test("process true", testProcessTrue),
Test("process false", testProcessFalse),
Test("container mount propagation", testContainerMountPropagation),
Test("process echo hi", testProcessEchoHi),
Test("process no executable", testProcessNoExecutable),
Test("process user", testProcessUser),
Expand Down