Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f10eaca
Resolve test directory paths before reaping per-test files
MayCXC Aug 1, 2026
6499f7d
Say how to run the tests that macOS skips
MayCXC Aug 2, 2026
bf4d7dc
Test against the kernel release the shipped tool installs
MayCXC Aug 2, 2026
21f1f16
Run each integration test in its own scratch directory
MayCXC Aug 12, 2026
d244d7a
Give the machine's storage the shape its containers have
MayCXC Aug 12, 2026
b3d7208
Add guest swap support
MayCXC Aug 1, 2026
d471d28
Give a pod a swap area its containers share
MayCXC Aug 2, 2026
c0d0220
Hold a container to the swap limit it was given
MayCXC Aug 2, 2026
2cbb723
Take the swap total as given when memory is unlimited
MayCXC Aug 2, 2026
be4f9cc
Let a pod's containers copy files in and out
MayCXC Aug 7, 2026
8f7a007
Give a pod the machine it asked for
MayCXC Aug 7, 2026
ea2a9a2
Let a pod's containers be given a writable layer
MayCXC Aug 7, 2026
9adf57c
Name the project the guest file transfer belongs to
MayCXC Aug 8, 2026
bf57208
Let a pod give a container's place back
MayCXC Aug 10, 2026
a72170e
Let a caller set the runtime spec's hooks
MayCXC Aug 2, 2026
12bf1f6
Let a pod's containers reach an OCI runtime and its hooks
MayCXC Aug 2, 2026
13a720f
Create the devices a container's runtime spec asks for
MayCXC Aug 2, 2026
3bb548c
Let a pod's containers be given devices
MayCXC Aug 7, 2026
5feab02
Let Virtualization take a disk into a running machine
MayCXC Aug 7, 2026
80ac5da
Give the guest kernel the drivers a hotplugged disk needs
MayCXC Aug 7, 2026
85ba74b
Let a disk taken onto a bus without a rescan wait to be enumerated
MayCXC Aug 8, 2026
ba26b76
Read a machine's mounts from the provider that registers them
MayCXC Aug 8, 2026
98293e1
Export directory shares to a running machine
MayCXC Aug 12, 2026
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
36 changes: 36 additions & 0 deletions Sources/CShim/include/swap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright © 2026 Apple Inc. and the Containerization 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.
*/

#ifndef __SWAP_H
#define __SWAP_H

#if defined(__linux__)

// swapon(2) lives in <sys/swap.h>, which Swift's glibc modulemap does not
// carry, so it is reachable from Swift only through a wrapper.

// Discard the whole area when it is enabled, and each page as it is freed.
// These come from the same header, and SWAP_FLAG_DISCARD_PAGES has no UAPI
// header of its own at all, so every consumer declares it; see util-linux
// sys-utils/swapon.c.
#define CZ_SWAP_DISCARD 0x10000
#define CZ_SWAP_DISCARD_PAGES 0x40000

int CZ_swapon(const char *path, int flags);

#endif

#endif
23 changes: 23 additions & 0 deletions Sources/CShim/swap.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 Apple Inc. and the Containerization 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.
*/

#ifdef __linux__
#include <sys/swap.h>

#include "swap.h"

int CZ_swapon(const char *path, int flags) { return swapon(path, flags); }
#endif
81 changes: 40 additions & 41 deletions Sources/Containerization/CHHotplugProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import Synchronization
///
/// Handles both block (`vm.add-disk`) and virtiofs (`vm.add-fs`, with one
/// `virtiofsd` per unique source-hash tag) hotplug, plus the matching
/// `vm.remove-device` teardown. Owns the per-VM mount registry so
/// `CHVirtualMachineInstance.mounts` can forward to it.
/// `vm.remove-device` teardown. Owns the machine's storage so
/// `CHVirtualMachineInstance.storage` can forward to it.
final class CHHotplugProvider: HotplugProvider {
struct HotplugRecord: Sendable {
let chDeviceId: String
Expand All @@ -50,7 +50,7 @@ final class CHHotplugProvider: HotplugProvider {
private let workDir: URL
private let virtiofsdBinaryOverride: URL?
private let allocator: any AddressAllocator<Character>
private let _mounts: Mutex<[String: [AttachedFilesystem]]>
private let _storage: Mutex<MachineAttachments>
private let _records: Mutex<[String: [HotplugRecord]]>
private let _tags: Mutex<[String: VirtiofsdTagState]>
/// Serializes per-tag virtiofsd spawn so a concurrent hotplug for the
Expand All @@ -65,14 +65,14 @@ final class CHHotplugProvider: HotplugProvider {
workDir: URL,
virtiofsdBinary: URL?,
allocator: any AddressAllocator<Character>,
initialMounts: [String: [AttachedFilesystem]],
initialStorage: MachineAttachments,
logger: Logger?
) {
self.client = client
self.workDir = workDir
self.virtiofsdBinaryOverride = virtiofsdBinary
self.allocator = allocator
self._mounts = Mutex(initialMounts)
self._storage = Mutex(initialStorage)
self._records = Mutex([:])
self._tags = Mutex([:])
self.spawnLock = AsyncLock()
Expand All @@ -81,14 +81,14 @@ final class CHHotplugProvider: HotplugProvider {

// MARK: - Read accessors

var mounts: [String: [AttachedFilesystem]] {
_mounts.withLock { $0 }
var storage: MachineAttachments {
_storage.withLock { $0 }
}

func withMountRegistry<T: Sendable>(
_ body: (inout sending [String: [AttachedFilesystem]]) throws -> sending T
func withStorage<T: Sendable>(
_ body: (inout sending MachineAttachments) throws -> sending T
) rethrows -> T {
try _mounts.withLock(body)
try _storage.withLock(body)
}

// MARK: - HotplugProvider conformance
Expand Down Expand Up @@ -150,13 +150,14 @@ final class CHHotplugProvider: HotplugProvider {
}
}

func registerMounts(id: String, rootfs: AttachedFilesystem, additionalMounts: [Mount]) throws {
var attached: [AttachedFilesystem] = [rootfs]
func registerMounts(id: String, rootfs: AttachedFilesystem, writableLayer: AttachedFilesystem?, additionalMounts: [Mount]) throws {
var mounts: [AttachedFilesystem] = []
for mount in additionalMounts {
attached.append(try AttachedFilesystem(mount: mount, allocator: allocator))
mounts.append(try AttachedFilesystem(mount: mount, allocator: allocator))
}
_mounts.withLock {
$0[id, default: []].append(contentsOf: attached)
let container = ContainerAttachments(rootfs: rootfs, writableLayer: writableLayer, mounts: mounts)
_storage.withLock {
$0.containers[id] = container
}
}

Expand Down Expand Up @@ -190,19 +191,10 @@ final class CHHotplugProvider: HotplugProvider {
}
}

// Drop block-derived AttachedFilesystem entries for `id`. Block entries
// are the ones whose source was rewritten to "/dev/vd<letter>" by
// `hotplug(_:)` (or by AttachedFilesystem(mount:allocator:) for an
// additionalMount of type virtio-blk).
_mounts.withLock { state in
guard var perID = state[id] else { return }
perID.removeAll { $0.source.hasPrefix("/dev/vd") }
if perID.isEmpty {
state.removeValue(forKey: id)
} else {
state[id] = perID
}
}
// The container's devices are gone, so the container leaves the
// registry with them; its shares are released separately and their
// processes reference-counted through `_tags`.
_ = _storage.withLock { $0.containers.removeValue(forKey: id) }
}

func hotplugVirtioFS(_ mounts: [Mount], id: String) async throws {
Expand All @@ -226,7 +218,7 @@ final class CHHotplugProvider: HotplugProvider {
let chDeviceId = try await ensureVirtiofsDevice(tag: tag, source: source, readonly: readonly)
// Record once per tag for this container. The AttachedFilesystem
// entries for these mounts are written by registerMounts (the sole
// _mounts writer), so we do NOT touch _mounts here.
// registry writer), so we do NOT touch the storage here.
_records.withLock {
$0[id, default: []].append(HotplugRecord(chDeviceId: chDeviceId, kind: .virtiofs(tag: tag)))
}
Expand Down Expand Up @@ -338,16 +330,17 @@ final class CHHotplugProvider: HotplugProvider {
try? FileManager.default.removeItem(at: socket)
}

// Drop virtiofs AttachedFilesystem entries for `id`. AttachedFilesystem
// sets `type = mount.type` which for a `.virtiofs` mount is "virtiofs".
_mounts.withLock { state in
guard var perID = state[id] else { return }
perID.removeAll { $0.type == "virtiofs" }
if perID.isEmpty {
state.removeValue(forKey: id)
} else {
state[id] = perID
// Drop the container's virtiofs entries. A container whose rootfs is
// itself a share leaves the registry whole; one that keeps block
// devices keeps its entry with the share entries dropped.
_storage.withLock { state in
guard var container = state.containers[id] else { return }
if container.rootfs.type == "virtiofs" {
state.containers.removeValue(forKey: id)
return
}
container.mounts.removeAll { $0.type == "virtiofs" }
state.containers[id] = container
}
}

Expand All @@ -358,15 +351,21 @@ final class CHHotplugProvider: HotplugProvider {
/// is the user-supplied `FsConfig.id` (which `vm.remove-device` keys on).
/// `ownerIds` are the container ids that count toward this tag's refcount;
/// each gets a `HotplugRecord` so `releaseVirtioFS(id:)` walks them
/// uniformly.
/// uniformly. `machineHeld` adds one reference nothing releases, for a
/// share the machine itself owns (a volume).
func recordBootTimeVirtiofs(
tag: String,
process: VirtiofsdProcess,
chDeviceId: String,
ownerIds: [String]
ownerIds: [String],
machineHeld: Bool
) {
_tags.withLock {
$0[tag] = VirtiofsdTagState(process: process, refcount: ownerIds.count, chDeviceId: chDeviceId)
$0[tag] = VirtiofsdTagState(
process: process,
refcount: ownerIds.count + (machineHeld ? 1 : 0),
chDeviceId: chDeviceId
)
}
_records.withLock { records in
for id in ownerIds {
Expand Down
Loading