I've added an optional progress handler for rootfs unpacking so that
consumers can show progress. The Ubuntu image takes about 8 seconds to
unpack on my machine, and I'm developing an app where it would be useful
to show progress in the UI.
Total size is determined in an optional first pass that scans archive
headers. Bytes written are then reported during unpacking. The optional
first pass adds 15 (Alpine) to 115 (Ubuntu) ms to unpacking duration on
my machine, depending on image size.
The memory limit (clamp to system ram) is a VZ limitation so it's weird
that we had that logic directly in LinuxContainer. Someone could make a
VZVirtualMachineInstance that doesn't have this limitation but we
inherit this logic because it's done before passing off the memory
amount to the VMM.
finishEncoding() called archive_free() and then passed the freed pointer
to wrap(), which would call archive_error_string() on it if the return
code was not ARCHIVE_OK. Fix by nil-ing the pointer before freeing and
throwing directly without attempting to read the (already freed) error
string.
## What
Add a `DNS.validate()` method that verifies all nameserver entries are
valid IPv4 or IPv6 addresses. The method is called from
`Vminitd.configureDNS()` before applying the configuration.
## Why
Closes#467. Currently, any arbitrary string can be passed as a
nameserver in `DNSConfiguration`, which silently results in an invalid
`/etc/resolv.conf` inside the container. Hostname strings like
`dns.example.com` would be written to resolv.conf but would not work as
nameservers.
## How
- Added `DNS.validate() throws` method in `DNSConfiguration.swift` that
iterates over all nameservers and attempts to parse each as either an
`IPv4Address` or `IPv6Address` (using the existing parsers in
`ContainerizationExtras`)
- Added `import ContainerizationExtras` to `DNSConfiguration.swift` (the
`Containerization` target already depends on `ContainerizationExtras`)
- Called `config.validate()` at the start of `Vminitd.configureDNS()` so
validation happens before any GRPC call
## Testing
Added 6 new unit tests in `DNSTests.swift`:
- ✅ Valid IPv4 nameservers accepted
- ✅ Valid IPv6 nameservers accepted
- ✅ Mixed IPv4/IPv6 nameservers accepted
- ✅ Empty nameserver list accepted
- ❌ Hostname rejected
- ❌ Invalid address rejected
## Checklist
- [x] Tests added
- [x] No breaking changes to existing API (validate() is a new method,
existing init is unchanged)
- [x] Follows existing patterns (uses ContainerizationExtras IP address
types, ContainerizationError for errors)
Signed-off-by: Maxime Grenu <maxime.grenu@gmail.com>
- Fixes EXT4 timestamp encoding for pre-1970 dates
- Fixes EXT4 timestamp decoding for pre-1970 dates
- Fixes the creation date
- Fixes a `UInt32` overflow
Resolves the following failures in the added tests:
```
Swift/arm64e-apple-macos.swiftinterface:38198: Fatal error: Double value cannot be converted to UInt64 because the result would be less than UInt64.min
error: Process '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/libexec/swift/pm/swiftpm-testing-helper --test-bundle-path /Users/Dmitry/Apple/containerization/.build/arm64-apple-macosx/debug/containerizationPackageTests.xctest/Contents/MacOS/containerizationPackageTests --filter encodeNegativeTimestamp /Users/Dmitry/Apple/containerization/.build/arm64-apple-macosx/debug/containerizationPackageTests.xctest/Contents/MacOS/containerizationPackageTests --testing-library swift-testing' exited with unexpected signal code 5
```
```
✘ Test decodeNegativeTimestamp() recorded an issue at TestEXT4Format+Create.swift:100:6: Caught error: not a valid EXT4 superblock
✘ Test decodeNegativeTimestamp() failed after 0.003 seconds with 1 issue.
✘ Suite NegativeTimestampRoundtripTests failed after 0.004 seconds with 1 issue.
✘ Test run with 1 test in 1 suite failed after 0.004 seconds with 1 issue.
```
The network protocol and VMNetNetwork implementation currently are
housed on ContainerManager even though they are generally useful types
even outside of this easy to use helper type. This change moves them to
not be nested types anymore, as well as exposes a new param on
VMNetNetworks constructor so we can pass the type of network.
Fix a bug in ArchiveWriter where large files would only partly be
written into an archive.
This is done so by writing the data in chunks.
---------
Signed-off-by: Aditya Ramani <a_ramani@apple.com>
- No real notable API changes. The largest change code-wise is in
Vminitd. The connection loop is set up immediately in the new API by
calling runConnections() on the client, and this blocks until either the
task it's running on is cancelled or beginGracefulShutdown is called.
Because of this, either we'd have to modify our API to have
VirtualMachineAgent implementations call some run() method, or we just
put runConnections in an internal task on Vminitd. This change uses the
latter approach as it's simpler.
- Updated dependencies to pick up latest protobuf and grpc-swift-2
fixes.
- ClientBootstrap.withConnectedSocket(fd).wait() registers the vsock fd
with epoll/kqueue immediately. vminitd sends its HTTP/2 SETTINGS frame
right away upon accepting the connection, so those bytes can arrive on
the fd — and be read by NIO — before the gRPC pipeline is installed.
With a bare pipeline, NIO has nowhere to send the bytes and discards
them.
- Buffer until ClientConnectionHandler is in the pipeline so it can
process the SETTINGS frame and fire .ready, then schedule delivery of
the buffered data with assumeIsolatedUnsafeUnchecked().execute.
- To help produce more accurate merged logs, defines a
`StderrLogHandler` in the vminitd Application.swift that writes
timestamps to the boot log with millisecond precision and format
identical to that of the log files created using the `--log-root` arg on
`container`.
- Updates hawkeye to latest version (v6.5.1).
---------
Co-authored-by: Danny Canter <danny_canter@apple.com>
### Summary
`rootfs` create writes proc/self/exe as a symlink, but currently passes
the previous regular file buffer to `ArchiveWriter.writeEntry`. This
change switches that symlink entry to nil, which matches archive writer
expectations.
### Test
Archive tests pass.
ContainerizationArchive internally used a `FileArchiveWriterDelegate`
type that defined callback methods which were handed to
`archive_write_open2` from libarchive
```
int archive_write_open2(struct archive *, void *,
archive_open_callback *, archive_write_callback *,
archive_close_callback *, archive_free_callback *);
```
Turns out those methods were at the very least partly broken, when used
alongside gzip compression.
Utilities like gnutar and gunzip couldn't entirely process the archives
produced by them, leading to errors like
```
➜ gtar -tvzf rootfs.tar.gz
...
drwxr-xr-x 0/0 0 2026-03-07 11:24 sbin/
-rwxr-xr-x 0/0 122451504 2026-03-07 11:24 sbin/vminitd
-rwxr-xr-x 0/0 121138856 2026-03-07 11:24 sbin/vmexec
gzip: (stdin): trailing garbage ignored
..
gtar: Child returned status 2
gtar: Error is not recoverable: exiting now
```
libarchive provides alternate APIs, `archive_write_open_filename` and
`archive_write_open_fd` where these callbacks are defined within the
library.
Lets use those instead and let the library handle this / future edge
cases (in this particular case it was adding some padding at the end of
the gzipped compressed file)
Now that the static linux SDK we use has libarchive linked against it,
we can finally live our dream (/s) of supporting copying directories
in/out a little easier. This was.. kind of annoying. I think the very
simple route of writing archive to guest/host -> streaming over grpc is
simplest, but on the guest end we have a couple problems:
1. The VMs rootfs is read only today always, which is a good thing to me
and I don't want to change if we don't have to.
2. Writing the archive to the containers rootfs temporarily could work,
but it's a bit weird, and the user can make the containers rootfs
readonly which would screw that plan.
3. We could write it to /run or /tmp, but they're tmpfs and dealing with
the headache of the user possibly tarring an enormous dir is one I don't
want to care about.
So, that leaves us with the (truthfully better to me) approach of trying
to write the tar data directly to the host and skipping grpc which kinda
forces us to have a temp spot. Because of that, I made it such that we
pass a port number from host<->guest, and transfer the actual data
(either the single file or tarred dir contents) over the vsock port
instead. The stream is kinda clunky, but it just serves as a means to
exchange metadata and a "we're done" signifier.
Closes#221
This contains (in the static linux sdk but the swift version and the sdk
must match) libarchive support and mimalloc! This also (unrelated to the
sdk bump it seems) raises the amount of retries we do when dialing
vminitd. It seems CI on rare occasions recently has timed out on some
tests. We could also explore just lowering the concurrency on the
integration suite.
- Will be used by the fix for apple/container#1253.
- We use a common keychain ID between the `container CLI and
`container-core-images`, but right now keychain entries created by the
CLI must be opted-in for `container-core-images`.
- Using an access group with a notarized application should give both
apps automatic access to the keychain.
Closes#256
If the target of the uds proxy goes through a symlink (/var/run) we had
the same bug we had with mounts previously where we'd follow this and if
the resolved path existed in the root of the VM we'd end up mounting
there. Fix this by plopping the sockets in a holding spot and then bind
mounting them in (which has the correct resolution logic).
This change fixes an issue in linux pod where the exec process
does not inherit the container process config which means any
environment variables, working directory, user, rlimits,
capabilities would be different for the exec process.
This change ensures that exec process would inherit
environment variables, working directory, user, rlimits,
and capabilities from the container process config but
resets arguments, terminal, and stdio as those should not
be inherited.
This change also adds an integration test to exercise this.
Just like we have for hosts and dns configurations, we can add a
hostname field to make it easier to setup if every container is going to
be using the same hostname.
Today we don't resolve symlinks relative to the rootfs of the container
as ContainerizationOS.Mount did not handle this. This means trying to
mount something at /var/run (which typically is a symlink to /run in
most cases) would resolve to /run in the root of the VM. This change
adds some logic to handle this.
```
➜ ./bin/cctl run --kernel bin/kernel.arm64 --mount /Users/dcantah/foo:/var/run/foo bash
root@sandbox-vm:/# ls var/run/foo/
howdy
```
- Closes#532.
- Relocate all zstd calls down into CArchive, and export a function
zstd_decompress_fd(src_fd, dst_fd) that ContainerizationArchive can use.
This eliminates problems that can occur when treating Swift warnings as
errors in Xcode projects that use ContainerizationArchive.
This PR adds OCI Image Spec v1.1 artifact support. It extends Manifest,
Index, and Descriptor with the `subject` and `artifactType` fields, adds
a `referrers()` method to RegistryClient implementing the OCI
Distribution Spec v1.1 referrers API. Also, I've added some unit unit
tests for backward compatibility and roundtrip encpding of all new
fields
This PR adds a `networking: Bool = true` parameter to
`ContainerManager.create()` so callers can opt out of network interface
creation on a per-container basis.
## Motivation
Currently, `ContainerManager.create()` unconditionally allocates a vmnet
network interface for every container (when the manager has a network
configured). Some use cases don't need network access and benefit from
having it disabled to reduce attack surface.
There's no way to achieve this today without either:
- Initializing the `ContainerManager` without a network (which disables
networking for *all* containers)
- Clearing `config.interfaces` in the configuration closure (which
wastes an IP allocation from the vmnet pool since `network.create(id)`
has already been called)
## Changes
- Add `networking: Bool = true` to all three `create()` overloads on
`ContainerManager`
- When `false`, `self.network?.create(id)` is skipped entirely. No
interface is allocated, and no DNS is configured.
- `releaseNetwork`/`delete` remain safe to call regardless, since
`Allocator.release` silently ignores unknown IDs.
- Add unit test `testNetworkingFalseSkipsInterfaceCreation` using the
existing `NilGatewayNetwork` fixture
- Add integration tests `testNetworkingDisabled` and
`testNetworkingEnabled` that create containers through a network-enabled
`ContainerManager` and verify the presence/absence of `eth0` via
`/sys/class/net/`
There are some workloads that benefit from having a "true" init process
that reaps zombies. This change implements a minimal init as part of
vminitd and exposes an API to be able to use this init process in our
containers. signals will be forwarded, any child procs will be reaped
and the exit code of the actual workload will be propagated. The
LinuxContainer/LinuxPod API is a very simple bool to ask to use this as
our init process, and the init simply spawns whatever command you want
as a child underneath this.
```
➜ ./bin/cctl run --kernel bin/kernel.arm64 --init
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /.cz-init -- /bin/sh
2 root 0:00 /bin/sh
3 root 0:00 ps aux
```
## What
Raise the constant from 127 to 255 in `Reference.swift` and update the
derived `referenceTotalLengthMax` accordingly.
## Why
Closes#453.
The [OCI distribution
spec](https://github.com/opencontainers/distribution-spec/blob/main/spec.md)
states that the **name** component of an image reference (registry host
+ repository path) may be at most 255 bytes. The previous hard-coded
limit of 127 characters incorrectly rejected valid references that used
long registry hostnames or deep path hierarchies.
The old `referenceTotalLengthMax = 255` was also inconsistent: with a
127-char name cap, a 255-char total reference would only allow a very
short tag. The new value is derived explicitly as `nameTotalLengthMax
(255) + separator (1) + tagLengthMax (128) = 384`.
## How
- `nameTotalLengthMax`: 127 → 255
- `tagLengthMax`: new constant (128) documenting the maximum tag length
already enforced by the tag regex (`{0,127}` + leading char = 128
chars).
- `referenceTotalLengthMax`: computed from the two constants above (384)
rather than hard-coded to 255.
## Testing
Added three new cases in `ReferenceTests.swift`:
- ✅ Name of 128 characters (registry + path) — previously rejected, now
accepted
- ✅ Name of exactly 255 characters — at the OCI spec maximum, accepted
- ❌ Name of 256 characters — one over the limit, rejected