Repository navigation
fs/macos: preserve the inode a rename overwrites - #700
Conversation
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the macOS passthrough filesystem implementation to handle overwritten targets during rename operations. It preserves the file descriptor of the overwritten target before renaming to avoid dangling volfs paths, and updates store_unlinked_fd to return a boolean indicating if the file descriptor was successfully stored. The reviewer identified a critical bug where do_unlink was not updated to handle the new return type, resulting in a file descriptor leak when store_unlinked_fd returns Ok(false). Additionally, the reviewer suggested using swap instead of store in store_unlinked_fd to prevent double-store leaks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| data.unlinked_fd | ||
| .store(unlinked_fd as i64, Ordering::Release); | ||
| // The tracked inode now owns `unlinked_fd` (closed in `forget_one`). | ||
| Ok(true) |
There was a problem hiding this comment.
1. Missed Update in do_unlink (Critical Bug)
Changing store_unlinked_fd to return io::Result<bool> requires all callers to handle the bool return value to avoid leaking the file descriptor when there is no tracked inode. While the rename path was updated correctly, do_unlink (around line 892) was missed and still ignores the return value:
if let Err(err) = self.store_unlinked_fd(unlinked_fd) {
unsafe { libc::close(unlinked_fd) };
warn!("Couldn't store unlinked fd \"{}\": {err}", unlinked_fd);
}Since Ok(false) is not an Err, the file descriptor is leaked in the unlink path when there is no tracked inode. Please update do_unlink to handle the return value as follows:
match self.store_unlinked_fd(unlinked_fd) {
Ok(true) => {}
Ok(false) => {
unsafe { libc::close(unlinked_fd) };
}
Err(err) => {
unsafe { libc::close(unlinked_fd) };
warn!("Couldn't store unlinked fd \"{}\": {err}", unlinked_fd);
}
}2. Preventing Double-Store Leak
If store_unlinked_fd is called multiple times for the same tracked inode (e.g., when multiple hard links are unlinked or overwritten), the previous file descriptor stored in unlinked_fd will be silently overwritten and leaked. Using swap instead of store allows us to retrieve and safely close any previously stored file descriptor.
let old_fd = data.unlinked_fd.swap(unlinked_fd as i64, Ordering::AcqRel);
if old_fd >= 0 {
unsafe { libc::close(old_fd as RawFd) };
}
// The tracked inode now owns unlinked_fd (closed in forget_one).
Ok(true)Add regression test for PR libkrun#700, which fixed a file descriptor leak on macOS when renaming over an existing file with open fds. The bug: macOS virtio-fs uses volfs paths (/.vol/{dev}/{ino}) which become invalid when the inode's last link is removed. When rename() replaces a file, the old target loses its directory entry, breaking subsequent operations on still-open file descriptors. This broke apt/dpkg atomic writes with errors like: "Problem closing the file /var/lib/dpkg/status - close (2: No such file or directory)" The fix stores an fd to the doomed target before rename, preserving access to the unlinked inode for tracked fds. The test verifies that fchmod() works on an open fd after the file has been atomically replaced via rename. Assisted-by: Claude Code:claude-opus-4.6 Signed-off-by: Sergio Lopez <slp@redhat.com>
f9d504e to
dd0ed3c
Compare
|
Thanks for the review — both points addressed and folded into the original commit (per the repo's squash-fixups-into-the-relevant-commit policy):
|
dd0ed3c to
5842e6c
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request prevents file descriptor leaks on macOS when inodes are unlinked or overwritten during rename operations by tracking and closing doomed file descriptors. The reviewer identified a critical race condition in store_unlinked_fd where the read lock on self.inodes is released prematurely, potentially allowing a concurrent forget request to remove the inode and leak the file descriptor. Holding the read lock during the swap operation was suggested to resolve this issue.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if let Some(data) = self.inodes.read().unwrap().get_alt(&altkey).cloned() { | ||
| data.unlinked_fd | ||
| .store(unlinked_fd as i64, Ordering::Release); | ||
| // Swap rather than store so that if this inode already had a | ||
| // preserved fd (e.g. another hard link was unlinked/overwritten | ||
| // earlier), we recover and close it instead of leaking it. | ||
| let old_fd = data.unlinked_fd.swap(unlinked_fd as i64, Ordering::AcqRel); | ||
| if old_fd >= 0 { | ||
| unsafe { libc::close(old_fd as RawFd) }; | ||
| } | ||
| // The tracked inode now owns `unlinked_fd` (closed in `forget_one`). | ||
| Ok(true) | ||
| } else { | ||
| // No tracked inode for this (dev, ino): the caller keeps ownership | ||
| // of `unlinked_fd` and must close it to avoid a leak. | ||
| Ok(false) | ||
| } |
There was a problem hiding this comment.
There is a potential race condition here that can lead to a silent file descriptor leak.
The Race Condition:
store_unlinked_fdacquires a read lock onself.inodes, clones theInodeDataArc, and then immediately drops the read lock because the temporaryRwLockReadGuardis dropped at the end of theif letcondition.- Before
store_unlinked_fdperforms the atomicswapondata.unlinked_fd, a concurrentforgetrequest can acquire the write lock onself.inodes, decrement the refcount of the same inode to0, close itsunlinked_fd(which is still-1), and remove the inode from the map. store_unlinked_fdthen resumes and swaps the newunlinked_fdinto the cloneddata.unlinked_fd. Since the inode has already been removed from the map, this file descriptor will never be closed byforget_oneand is leaked forever.
The Solution:
By holding the read lock on self.inodes during the swap operation, we prevent forget from acquiring the write lock and removing the inode until the swap is complete. This also avoids the need to clone the Arc<InodeData>.
let inodes = self.inodes.read().unwrap();
if let Some(data) = inodes.get_alt(&altkey) {
// Swap rather than store so that if this inode already had a
// preserved fd (e.g. another hard link was unlinked/overwritten
// earlier), we recover and close it instead of leaking it.
let old_fd = data.unlinked_fd.swap(unlinked_fd as i64, Ordering::AcqRel);
if old_fd >= 0 {
unsafe { libc::close(old_fd as RawFd) };
}
// The tracked inode now owns unlinked_fd (closed in forget_one).
Ok(true)
} else {
// No tracked inode for this (dev, ino): the caller keeps ownership
// of unlinked_fd and must close it to avoid a leak.
Ok(false)
}5842e6c to
6362659
Compare
|
Folded the race-condition fix into the original commit (per the squash-fixups policy).
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request addresses a potential file descriptor leak and dangling volfs path issue on macOS during file renames that overwrite existing targets. It updates store_unlinked_fd to return a boolean indicating whether the tracked inode took ownership of the file descriptor, and updates the rename logic to grab and store the doomed file descriptor of the overwritten target. The reviewer suggested a safer approach when opening the parent directory path by explicitly specifying libc::O_RDONLY and libc::O_DIRECTORY flags.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let parent_fd = unsafe { | ||
| libc::open(parent_cpath.as_ptr(), libc::O_NOFOLLOW | libc::O_CLOEXEC) | ||
| }; |
There was a problem hiding this comment.
When opening the parent directory path, it is safer and more robust to explicitly specify libc::O_RDONLY and libc::O_DIRECTORY. This ensures that the operation only succeeds if the path is indeed a directory, and explicitly documents the read-only intent of the file descriptor.
let parent_fd = unsafe {
libc::open(
parent_cpath.as_ptr(),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};The macOS virtio-fs passthrough addresses inodes by their volfs path
("/.vol/{dev}/{ino}"), which only resolves while the inode still has a
directory entry. A rename that REPLACES an existing target drops that
target's last link, but unlike do_unlink -- which stashes an fd to the
doomed inode in InodeData.unlinked_fd -- rename never preserved the
overwritten inode. Any FUSE op the guest later issues on it then resolves
a dangling volfs path and returns ENOENT.
This breaks the common atomic write-new-then-rename-over pattern when the
guest still holds the old target open: apt/dpkg rewrite /var/lib/dpkg/status
this way while apt holds it open, so apt's final close reports
"Problem closing the file /var/lib/dpkg/status - close (2: No such file or
directory)" even though the install succeeded. It reproduces on any
overlayfs upperdir backed by this server.
Mirror do_unlink: grab an fd to the target before renamex_np (replace case
only -- not RENAME_SWAP/RENAME_EXCL) and hand it to the displaced inode on
success. store_unlinked_fd now reports whether a tracked inode took
ownership so the caller closes the fd otherwise instead of leaking it.
Signed-off-by: Alex NOVAC <acdtrx@gmail.com>
Add regression test for PR libkrun#700, which fixed a file descriptor leak on macOS when renaming over an existing file with open fds. The bug: macOS virtio-fs uses volfs paths (/.vol/{dev}/{ino}) which become invalid when the inode's last link is removed. When rename() replaces a file, the old target loses its directory entry, breaking subsequent operations on still-open file descriptors. This broke apt/dpkg atomic writes with errors like: "Problem closing the file /var/lib/dpkg/status - close (2: No such file or directory)" The fix stores an fd to the doomed target before rename, preserving access to the unlinked inode for tracked fds. The test verifies that fchmod() works on an open fd after the file has been atomically replaced via rename. Assisted-by: Claude Code:claude-opus-4.6 Signed-off-by: Sergio Lopez <slp@redhat.com>
6362659 to
c7150b7
Compare
Add regression test for PR #700, which fixed a file descriptor leak on macOS when renaming over an existing file with open fds. The bug: macOS virtio-fs uses volfs paths (/.vol/{dev}/{ino}) which become invalid when the inode's last link is removed. When rename() replaces a file, the old target loses its directory entry, breaking subsequent operations on still-open file descriptors. This broke apt/dpkg atomic writes with errors like: "Problem closing the file /var/lib/dpkg/status - close (2: No such file or directory)" The fix stores an fd to the doomed target before rename, preserving access to the unlinked inode for tracked fds. The test verifies that fchmod() works on an open fd after the file has been atomically replaced via rename. Assisted-by: Claude Code:claude-opus-4.6 Signed-off-by: Sergio Lopez <slp@redhat.com> (cherry picked from commit 39ea6c3) Signed-off-by: Sergio Lopez <slp@redhat.com>
The 'Integration Tests (Linux x86_64)' CI job has been failing on all
recent PRs. The most basic test, configure-vm-2cpu-1GiB, produces an
empty stdout.txt despite the VM booting cleanly and the guest-agent
running successfully. The diagnostic log shows:
TRACE process_tx] tx unparked, queue len 0
INFO krun_vmm] Vmm is stopping.
The TX thread woke up, saw an empty queue, and exited — losing the
guest's final println!("OK") output.
Root cause: race in pop_head_blocking
--------------------------------------
The TX thread parks via thread::park() when the queue is empty and is
woken either by notify_tx() (from the event-manager thread, which fires
when the guest writes the virtio queue notification eventfd) or by
port::shutdown() (which sets stop=true then calls unpark()).
The old code:
loop {
match queue.pop(mem) {
Some(d) => break Some(d),
None => {
interrupt.signal_used_queue();
if stop.load(Acquire) { break None; } // exits here
thread::park();
}
}
}
The race window:
TX thread Shutdown thread Event-manager thread
─────────────────────────────────────────────────────────────────────
queue.pop() -> None
interrupt.signal_used_queue()
stop.load() -> false stop.store(true)
thread::park() tx_thread.unpark()
<- wakes
queue.pop() -> None (guest wrote "OK\n"; notification in flight)
stop.load() -> true
break None <- EXITS notify_tx() -> unpark()
[TX thread already gone]
The guest's final bytes are in the virtio avail ring when the TX thread
exits. The event-manager's notify_tx() arrives after termination. The
stdout.txt file is empty; check() returns TestOutcome::Fail.
Why only x86_64?
-----------------
The aarch64 self-hosted runner sets KRUN_NO_UNSHARE=1, skipping the
buildah-unshare + network-namespace wrapper. Without that wrapper the
per-test process lifetime is different (longer VM teardown path), making
the race window substantially narrower. It does not surface in practice
on aarch64.
Fix: drain the queue with the disable/enable_notification fence
---------------------------------------------------------------
The correct pattern — already used by the block and net device workers
(block/worker.rs, net/worker.rs) — is to suppress guest notifications
while draining, then call enable_notification() which does a SeqCst
fence and re-reads avail_idx. If new entries arrived while
notifications were suppressed, enable_notification returns true and we
drain again. Only after the fence confirms the queue is empty do we
check stop. If stop is true at that point, no bytes remain unprocessed
and we can return None safely.
The stop check is deliberately placed after the drain+fence: if
stop was set while the guest was writing its last bytes, we flush them
before exiting.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code: claude-sonnet-4-6
AGENTS.md: be explicit about incorrect Agent Attribution format
Be explicit in the AGENTS.md file about not using the Co-authored-by:
attribution format.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
.claude: fix path to AGENTS.md file
Based on the memory documentaiton, @AGENTS.md imports are resolved
relative to the file containing the import, not the working directory.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
vmm: fix riscv64 non-TEE memory region setup
In non-x86 builds that don't use TEE features, vstate.rs used cfg!() to
distinguish between TEE and non-TEE memory setup paths.
cfg!() only evaluates to a compile-time constant; code in both branches
still participates in compilation and type checking. As a result, normal
riscv64 builds still type-check guest_memfd and memory attribute code,
which leads to build failures.
Refactor the memory-region setup into cfg-gated helpers so non-TEE builds
do not compile TEE-only memory setup code, and reject unsupported
TEE+architecture combinations during VM setup instead of during memory
initialization.
Verify with:
cargo check -p vmm --target riscv64gc-unknown-linux-gnu
Signed-off-by: Zewei Yang <yangzewei@loongson.cn>
virtio-fs: add a regression test for scoped_cred restore
Drive the real ScopedUid through repeated per-request switches on a
thread set up like an unprivileged server holding CAP_SETUID (the
state that exposed the hardcoded euid-0 restore), and assert the
thread is left back at the server uid rather than wedged at the guest
uid. Credential changes are per-thread, so the test runs on a
dedicated thread and leaves the rest of the binary untouched.
It needs CAP_SETUID and a non-zero uid to switch to, so it runs under
the suite's buildah-unshare userns (or as root) and skips otherwise.
Against the pre-fix Drop it fails with the worker stuck at the guest
uid; with the fix it passes.
Signed-off-by: Hans Jørgen Hoel <hansjorg@urkilden.no>
Assisted-by: Claude Code: claude-fable-5
virtio-fs: restore the original thread credentials, not euid/egid 0
scoped_cred's Drop resets the worker thread's effective uid/gid to a
hardcoded 0, assuming the server runs as root. That breaks an
unprivileged server granted CAP_SETUID/CAP_SETGID (e.g. via systemd's
AmbientCapabilities=): set_creds sees the capability and switches, but
restoring to 0 poisons the thread. A first switched request's Drop parks
the thread at euid 0; the next switch to a non-zero uid clears the
thread's effective capabilities (capabilities(7), "Effect of user ID
changes on capabilities"); that request's Drop then fails EPERM, and the
thread is stuck with the guest uid's credentials. Every later request it
handles -- including ones for guest root -- fails EACCES/EPERM on host
files that uid cannot reach.
Any guest issuing requests as more than one uid wedges within a few ops.
A Debian guest running 'apt-get update' (whose fetch methods drop to the
_apt user) reproduces it: the InRelease/Packages renames fail with
'rename failed, Permission denied'.
Capture the effective id before switching and restore that on drop. A
root-run server captures 0 and is unchanged; an unprivileged server now
only switches between non-zero uids, never clearing its caps -- and the
worker thread no longer escalates itself to host euid 0.
Signed-off-by: Hans Jørgen Hoel <hansjorg@urkilden.no>
Assisted-by: Claude Code: claude-fable-5
lib: remove deprecated krun_set_root
Callers should use krun_add_virtiofs3() with KRUN_FS_ROOT_TAG
("/dev/root") instead, which provides explicit control over DAX
window size and read-only mode.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: replace krun_set_root with krun_add_virtiofs3
Replace all callers of krun_set_root with explicit
krun_add_virtiofs3(ctx, "/dev/root", path, 0, false) calls.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
include: remove stale implicit resource creation comment
All krun_disable_implicit_* functions are gone. The 2.0 API requires
explicit resource creation.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove krun_disable_implicit_vsock and implicit vsock creation
Vsock creation is now fully explicit via krun_add_vsock(). No vsock
device is created unless the caller requests one.
Remove the Implicit variant from VsockConfig, the implicit vsock
creation heuristics in krun_start_enter, and krun_disable_implicit_vsock.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: use explicit krun_add_vsock everywhere
- test_tsi_tcp_guest_connect: add krun_add_vsock(ctx, KRUN_TSI_HIJACK_INET)
- test_tsi_tcp_guest_listen: same
- test_vsock_guest_connect: add krun_add_vsock(ctx, 0)
- chroot_vm.c: replace krun_disable_implicit_vsock + vhost-user
with explicit krun_add_vsock when not using vhost-user-vsock
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove krun_disable_implicit_console, krun_set_console_output, and implicit console
Console creation is now fully explicit via krun_add_virtio_console_default
or krun_add_virtio_console_multiport. No console is created unless the
caller requests one.
Remove the disable_implicit_console field from VmResources, the implicit
console and serial device creation paths in builder.rs, the console_output
field and setter on VmResources, and krun_set_console_output (kept only
behind cfg(aws-nitro) where NitroEnclave still needs it).
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: use explicit krun_add_virtio_console_default everywhere
Replace implicit console creation with explicit
krun_add_virtio_console_default calls. Also replace
krun_set_console_output in nitro.c with
krun_add_virtio_console_default.
No test or example relies on implicit console injection anymore.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove unsupported krun_set_mapped_volumes
This function has been returning -EINVAL unconditionally. Remove it.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove deprecated krun_set_log_level
Superseded by krun_init_log which provides control over target fd,
log style, and env override options.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: propagate KRUN_NITRO_DEBUG flag in krun_init_log
krun_set_log_level set KRUN_NITRO_DEBUG when level==4 (debug), but
krun_init_log did not. Fix the omission so removing krun_set_log_level
doesn't regress nitro debug logging.
Also fix the condition to level >= 4 so that trace (level 5) also
enables nitro debug.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: replace krun_set_log_level with krun_init_log
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove deprecated krun_set_passt_fd, krun_set_gvproxy_path, krun_set_net_mac
These were replaced by krun_add_net_unixstream, krun_add_net_unixgram,
and krun_add_net_tap (which take mac as a parameter directly).
Also remove the internal LegacyNetworkConfig enum, legacy_net_cfg and
legacy_mac fields, the compat path in krun_start_enter that converted
them to the new net backend, and the now-unused NET_COMPAT_FEATURES
constant.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove deprecated krun_set_root_disk and krun_set_data_disk
These were replaced by krun_add_disk. Also remove the internal
root_block_cfg/data_block_cfg fields and their setters, and simplify
get_block_cfg() now that the legacy compat path is gone.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
examples: replace deprecated krun_set_root_disk/krun_set_data_disk with krun_add_disk
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init: remove init.c
Remove the init.c file since we are now using the rust implementation.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: loosen perms on /dev/kvm if it exists
Best effort loosen /dev/kvm permissions to allow nested virtualization
by unprivileged processes inside the microVM (usually a single purpose
environment). Log errors but don't log ENOENT since the guest kernel
may not support KVM or nested virtualization might not be enabled.
Port of containers/libkrun#708.
Assisted-by: Claude Code:opus-4.6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init/dhcp: only overwrite resolv.conf with DNS
Port of upstream commit 378e5249 ("init/dhcp: only overwrite
resolv.conf with DNS"). Only write /etc/resolv.conf when the DHCP
server provides nameservers, preserving any pre-existing content.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: match KRUN_INIT_PID1 by prefix rather than exact equality
The C init checked *env_init_pid1 == '1' (first-byte comparison),
accepting any value starting with '1' — including "10" or "1\n" (which
can appear when the value originates from a file read). The Rust port
used exact equality with "1", silently ignoring those variants.
Replace with is_ok_and(|v| v.starts_with('1')).
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: exit 125 when setup_redirects cannot open virtio-ports
The C init called exit(125) if setup_redirects() returned a negative
value (which it did when opendir("/sys/class/virtio-ports") failed).
The Rust port returned silently, letting the workload run with
unredirected stdio and no diagnostic.
Match the C behaviour: print an error and exit(125) so callers get a
visible signal that the redirects could not be set up.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: fork timesync child process instead of spawning a thread
A thread is destroyed when the parent calls execvp() (in PID1 mode).
The C init ran clock_worker() in a forked child process, which survives
exec. Match that behaviour: create the vsock socket, fork, and run the
recv loop in the child; the parent closes its copy of the socket and
returns immediately.
Also switch to nix wrappers throughout: socket::socket(), socket::recv(),
time::clock_gettime(), and time::clock_settime() replace the equivalent
unsafe libc calls. Add the nix "time" feature to support the clock
functions.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: document argv fallback divergence from C init
The C init replaced argv[0] with "/bin/sh" when neither KRUN_INIT nor
a config file was present, forwarding remaining cmdline tokens as shell
arguments. The Rust init instead treats proc_args[1] as the executable
directly.
Add a comment explaining the rationale: callers that omit both KRUN_INIT
and a config file intend the cmdline argument to be the command, not a
shell script path, making the Rust behaviour more intuitive.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: accept colon-separated rlimits and strip outer quotes
Two issues with apply_rlimits():
1. The C init parsed KRUN_RLIMITS with strtoull() and a single-char skip,
so any separator character between ID, CUR, and MAX worked (e.g.
"7:1024:4096"). The Rust code required "ID=CUR:MAX" and silently
skipped entries using the historical colon-only format.
2. krun_set_rlimits() wraps the entire value in double-quotes
(format!("\"{}\"", ...)), so the env var received by init is
"\"7=1024:4096\"". Neither the old Rust nor the C parser handled
this correctly.
Fix both by extracting parse_rlimit_entry() which strips outer '"' chars
and splits on the first two occurrences of '=' or ':' via splitn(3).
Both formats and the quoted form now parse correctly.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: add Entrypoint field and Docker-format config key aliases
The C init accepted "Cmd", "Env", "WorkingDir"/"Cwd", and "Entrypoint"
keys via case-insensitive comparison; the Rust port only handled OCI
runtime-spec keys ("args", "env", "cwd" inside "process").
Add serde aliases so RawConfig's flat fields also accept the Docker image
config capitalisation:
- "Cmd" aliases "args"
- "Env" aliases "env"
- "WorkingDir"/"Cwd" alias "cwd"
- new "Entrypoint" field (top-level, Docker format only)
When Entrypoint is present it is prepended to the resolved args vector,
matching the C init's concat_entrypoint_argv() behaviour.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: bring up loopback interface on FreeBSD
The non-Linux setup_network() stub was empty, so the lo interface was
never raised inside FreeBSD guests. The C init unconditionally brought up
lo on all platforms (the #if __linux__ guard covered only the DHCP block,
not lo setup).
Use nix::sys::socket to open an AF_INET/SOCK_DGRAM socket and issue
SIOCSIFFLAGS / IFF_UP, matching the C behaviour.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: set up a dummy network interface with TSI
Port of 2593accd9b57da23912d1824216bcc53866c838d. When TSI is active,
brings up dummy0 and assigns it 10.0.0.1/8 so applications that probe
for network availability see a configured interface. Silently skips
setup if the dummy driver is absent in the kernel.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: remove KRUN_REMOVE_ROOT_DIR_IOCTL
Port of cd8b2be0af316fe4c38b6749a888c56ca7699277. The temporary root
directory hack has been replaced by NullFs, so the ioctl that cleaned
it up is no longer needed.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: remove C sources and tee/ attestation code
Delete init/init.c, init/dhcp.c, init/dhcp.h, init/jsmn.h, and the
entire init/tee/ directory (snp_attest.c/h and the KBS client).
The amd-sev feature no longer performs LUKS unlock or KBS attestation —
it mounts /dev/vda as ext4 like the tdx path does.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: port timesync clock_worker to Rust
Implements the timesync feature behind the `timesync` cargo feature flag.
Receives host-side nanosecond timestamps over AF_VSOCK/SOCK_DGRAM on port
123 and applies them via clock_settime when the delta exceeds 100ms.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
ci: fix FreeBSD cross-compilation
Replace the C-based BSD init build rule (which referenced the now-deleted
init/init.c) with a cargo build rule targeting the correct Rust triple.
Makefile:
- Remove dead INIT_SRC = init/init.c variable.
- Derive FREEBSD_RUST_TARGET from the host ARCH with arm64→aarch64
substitution to get the correct Rust triple.
- Set CARGO_BSD_RUSTFLAGS with the clang cross-linker flags (mirroring
the existing CC_BSD setup) so cargo can link for FreeBSD.
- aarch64-unknown-freebsd is a Tier 3 target with no prebuilt std;
use +nightly -Z build-std for that case.
setup-build-env:
- Add rustup target add x86_64-unknown-freebsd (Tier 2, prebuilt std).
- Install nightly toolchain + rust-src for the aarch64 FreeBSD case.
cross-compilation.yml:
- Add clang to the Linux cross-compilation dependencies so the
FreeBSD linker flags resolve correctly on Linux runners.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: wire FreeBSD platform support
Connect the FreeBSD helpers into the boot sequence:
- open_console() and populate_env_from_kenv() are called at the very
start of main() before anything else
- setsid/TIOCSCTTY are Linux-only; open_console() handles session setup
on FreeBSD
- setlogin("root") is called on FreeBSD after console setup
- KRUN_DHCP and DHCP setup are Linux-only
- If KRUN_CONFIG is not set, mount_config_iso() is attempted; the ISO
is unmounted immediately after config::load() returns
- fs::* mounts and mount_shared_root are Linux-only
- exec_workload() calls open_console() on FreeBSD instead of
setup_redirects(), giving the child process a fresh controlling
terminal before execvp
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: add FreeBSD platform helpers
Add init/src/freebsd.rs with:
- kenv_get(): reads a variable from the FreeBSD kernel environment via
kenv(2), which is the source of env vars for init before the process
environment is set up
- populate_env_from_kenv(): imports the known KRUN_* variables from
kenv into std::env at startup so the rest of the code can use
std::env::var uniformly on both platforms
- open_console(): replicates login_tty(3) without linking libutil —
revokes existing opens of /dev/console, opens it, creates a new
session via setsid(2), sets the controlling terminal via TIOCSCTTY,
and dup2s it onto stdio; falls back to /dev/null + /init.log
- mount_config_iso() / unmount_config_iso(): mounts the KRUN_CONFIG
ISO 9660 image at /mnt via nmount(2) so the JSON config file can be
read, then unmounts it afterwards
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: wire main.rs with full boot sequence
Connect all modules in main() in order:
1. mount_block_root() [amd-sev | tdx]
2. mount_filesystems()
3. mount_block_root_device() [KRUN_BLOCK_ROOT_DEVICE]
4. mount_shared_root()
5. setsid + TIOCSCTTY
6. setup_network()
7. config::load()
8. mount_tmpfs() [config tmpfs mount]
9. apply_env / apply_hostname / apply_rlimits
10. chdir to workdir
11. run_workload(argv)
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement I/O redirects and workload launch
Add exec.rs with:
- setup_redirects(): walks /sys/class/virtio-ports and dup2s
krun-stdin/stdout/stderr onto the corresponding file descriptors
- set_exit_code(): reports the workload exit code to the host via
KRUN_EXIT_CODE_IOCTL, only when the root fs is virtiofs
- run_workload(): forks so PID 1 can reap children; the child calls
exec_workload() which sets up redirects and execvp's the argv.
Parent waits for the child, reports exit code, syncs, and reboots.
KRUN_INIT_PID1=1 skips the fork and exec_workload directly as PID 1.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement environment and resource setup
Extend env.rs with:
- apply_hostname(): sets hostname from HOSTNAME env var, defaulting
to "localhost"
- apply_env(): maps KRUN_HOME -> HOME and KRUN_TERM -> TERM
- apply_rlimits(): parses the KRUN_RLIMITS comma-separated list of
id,cur,max triples and applies each via setrlimit(2)
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement network interface setup
Add setup_network() and setup_dhcp() to env.rs.
setup_network() brings up lo unconditionally. setup_dhcp() checks that
the interface exists before calling do_dhcp(), and logs a warning on
failure rather than aborting (DHCP failure is non-fatal — the VM may be
IPv6-only or have no network).
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement JSON config parsing
Add init/src/config.rs, replacing the hand-rolled jsmn-based parser
with serde_json. Parses /.krun_config.json (or KRUN_CONFIG env var) and
returns a Config struct with:
- argv: Entrypoint ++ (args | Cmd), or None if absent
- workdir: WorkingDir or Cwd
- tmpfs: first tmpfs mount destination not already mounted
Environment variables from the Env array are applied during parsing,
with HOME and TERM always overwritten, all others set only if unset.
A missing or unparseable config file is silently ignored.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement DHCP client
Port init/dhcp.c to Rust in init/src/dhcp.rs. The public surface is a
single do_dhcp(iface) function with the same behaviour as the C version:
- Sends DHCPDISCOVER with Rapid Commit (option 80)
- On DHCPACK: applies address, route, MTU, and DNS directly
- On DHCPOFFER: completes the 4-way handshake, then applies
- On no response: returns Ok (VM may be IPv6-only)
Netlink structs not exposed by libc (ifinfomsg, ifaddrmsg, rtmsg) are
defined locally with #[repr(C)]. sockaddr_nl and sockaddr_in are
zero-initialised via mem::zeroed() to handle opaque padding fields.
Assisted-by: Claude Code:claude-sonnet-4.6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: implement KRUN_BLOCK_ROOT_DEVICE pivot and shared root mount
Extend fs.rs with:
- try_mount(): mounts with a known fstype, or probes /proc/filesystems
when fstype is None
- mount_block_root_device(): handles KRUN_BLOCK_ROOT_DEVICE by mounting
the block device at /newroot, issuing KRUN_REMOVE_ROOT_DIR_IOCTL to
drop the virtiofs temporary root, then pivoting with MS_MOVE
- mount_shared_root(): sets MS_REC|MS_SHARED propagation on /
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement filesystem mounting
Add init/src/fs.rs with:
- mount_or_busy(): helper that treats EBUSY as success
- mount_filesystems(): mounts devtmpfs, proc, sysfs, cgroup2,
devpts, tmpfs(/dev/shm), and creates the /dev/fd symlink
- is_mount_point(): parses /proc/mounts (avoids triggering Podman
auto-mounts that stat() would cause)
- mount_tmpfs(): mounts a tmpfs at an arbitrary path
Implement mount_tee_block_root() function used
by both SEV and TDX features to mount /dev/vda and chroot into it.
For amd-sev this replaces the previous LUKS/KBS attestation path
entirely. The SEV and TDX boot paths are now identical at the init level.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: scaffold Rust krun-init crate and wire into build system
Replace the C-based build_default_init() in src/devices/build.rs with a
Rust crate (init/) compiled via a cargo subprocess. The new build.rs
probes whether the active rustc supports the $(uname -m)-unknown-linux-musl
target (for a static binary) and falls back to the native target with a
user-visible warning if not.
The KRUN_INIT_BINARY_PATH override mechanism is preserved so that
out-of-tree binaries (e.g. pre-built SEV or TDX images) can still be
injected without rebuilding.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
ci: trust libkrun/krun tap before installing formulae on macOS
Homebrew now refuses to install formulae from third-party taps
that haven't been explicitly trusted, breaking all macOS CI jobs.
Assisted-by: Claude Code:claude-opus-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
AGENTS.md: fix agent attribution format
The agent attribution in each commit is missing the "Assisted-by"
prefix.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
virtio/balloon: add Windows support for free-page reporting
Use DiscardVirtualMemory as the Windows equivalent of madvise for releasing guest memory in the balloon free-page reporting queue. Gate the unix AsRawFd import and add the Windows shim for the event handler.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/console: split port_io into platform-specific modules
Replace the single port_io.rs file with a port_io/ directory containing mod.rs (shared traits and types), unix.rs, and windows.rs. The public API is unchanged; all platform-specific implementations are now isolated in their respective modules instead of being interleaved with #[cfg] annotations throughout a single file.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/console: add Windows support
Add Windows backends for console I/O using native HANDLEs instead of
Unix file descriptors. Introduce SendHandle in utils for thread-safe
handle passing, platform-specific PortInput/PortOutput/TerminalProperties
implementations using ReadFile/WriteFile/WaitForMultipleObjects, Windows
terminal raw mode via Get/SetConsoleMode, and the corresponding C API
surface with void* handle parameters behind #ifdef _WIN32.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
deps: cfg-gate unix-only crates and fix vm-memory for Windows
Move nix to [target.'cfg(unix)'.dependencies] in devices and vmm.
Disable vm-memory default features (the rawfd feature is unsupported
on Windows) across kernel, smbios, devices, and vmm. Add windows-sys
to devices, vmm, and libkrun for Windows console APIs. Bump imago
to 0.2.3.
Signed-off-by: lstocchi <lstocchi@redhat.com>
libkrun: expose init-blob in krun_has_feature()
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
Makefile: support INIT_BLOB=0 to disable bundled init binary
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
libkrun: add "init-blob" cargo feature to gate bundled init binary
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init_blob: align crate with the workspace
Rename directory from init-blob to init_blob, set edition to 2024,
rename the package to krun-init-blob, and set version to 0.1.0-1.18.0.
Signed-off-by: Sergio Lopez <slp@redhat.com>
fs/macos: on readdir, refresh directory on rewind
Use offset == 0 as an indicator that we're dealing with either a fresh
directory stream or one that has been rewound. If that's the case,
ensure the cache will be refreshed.
Suggested-by: Jay Gowdy <jgowdy@godaddy.com>
Signed-off-by: Sergio Lopez <slp@redhat.com>
tests/virtiofs-misc: add rename-overwrite fchmod subtest
Add regression test for PR #700, which fixed a file descriptor leak
on macOS when renaming over an existing file with open fds.
The bug: macOS virtio-fs uses volfs paths (/.vol/{dev}/{ino}) which
become invalid when the inode's last link is removed. When rename()
replaces a file, the old target loses its directory entry, breaking
subsequent operations on still-open file descriptors.
This broke apt/dpkg atomic writes with errors like:
"Problem closing the file /var/lib/dpkg/status - close (2: No such file or directory)"
The fix stores an fd to the doomed target before rename, preserving
access to the unlinked inode for tracked fds.
The test verifies that fchmod() works on an open fd after the file
has been atomically replaced via rename.
Assisted-by: Claude Code:claude-opus-4.6
Signed-off-by: Sergio Lopez <slp@redhat.com>
fs/macos: preserve the inode a rename overwrites
The macOS virtio-fs passthrough addresses inodes by their volfs path
("/.vol/{dev}/{ino}"), which only resolves while the inode still has a
directory entry. A rename that REPLACES an existing target drops that
target's last link, but unlike do_unlink -- which stashes an fd to the
doomed inode in InodeData.unlinked_fd -- rename never preserved the
overwritten inode. Any FUSE op the guest later issues on it then resolves
a dangling volfs path and returns ENOENT.
This breaks the common atomic write-new-then-rename-over pattern when the
guest still holds the old target open: apt/dpkg rewrite /var/lib/dpkg/status
this way while apt holds it open, so apt's final close reports
"Problem closing the file /var/lib/dpkg/status - close (2: No such file or
directory)" even though the install succeeded. It reproduces on any
overlayfs upperdir backed by this server.
Mirror do_unlink: grab an fd to the target before renamex_np (replace case
only -- not RENAME_SWAP/RENAME_EXCL) and hand it to the displaced inode on
success. store_unlinked_fd now reports whether a tracked inode took
ownership so the caller closes the fd otherwise instead of leaking it.
Signed-off-by: Alex NOVAC <acdtrx@gmail.com>
init: loosen perms on /dev/kvm if it exists
Best effort loosen /dev/kvm permissions to allow nested virtualization by
unprivileged processes inside the microVM (usually a single purpose
environment). Log errors but don't log ENOENT since the guest kernel may
not support KVM or nested virtualization might not be enabled.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Dusty Mabe <dusty@dustymabe.com>
devices: add WHP IOAPIC backend for Windows x86_64
WHP emulates the LAPIC but not the IOAPIC. Add a WhpIoapicBackend that
plugs into the common IOAPIC register emulation and delivers interrupts
through WHvRequestInterrupt.
The backend translates IOAPIC redirection-table entries into WHP
InterruptRequest structs, handling physical/logical destination modes,
edge/level trigger modes, and delivery modes (Fixed, LowestPriority, NMI,
INIT, SIPI). Level-triggered interrupts track Remote-IRR as the KVM
backend does.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
devices: refactor IOAPIC to support multiple hypervisor backends
This commit refactors the IOAPIC emulation logic to decouple common register state management from hypervisor-specific interrupt injection. This is a prerequisite for supporting the Windows Hypervisor Platform (WHP).
It introduces the IoApicBackend trait to handle backend-specific logic such as interrupt routing updates and injection.
It moves the register state into a dedicated IoApicRegs struct, making the core IoApic struct generic over the backend (IoApic<B: IoApicBackend>) and place KVM-sspecific implementation into a new ioapic_kvm.rs file.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/net: fix ENAMETOOLONG on long gvproxy socket paths
macOS imposes a 104-byte limit on unix socket paths. The gvproxy peer
socket path can be long enough that appending a suffix to derive the
local bind address exceeds this limit, triggering ENAMETOOLONG. This
surfaced as BadActivate from the net device, panicking the vCPU thread
and leaving the VM frozen at 0% CPU while krunkit stayed alive.
Place the local bind socket in the OS default temporary directory using a
short PID + per-process atomic counter filename (krun-net-<pid>-<n>.sock).
PID + counter guarantees uniqueness across concurrent krunkit processes
and multiple virtio-net devices within a single process.
Add a net-gvproxy-long-path integration test that constructs a peer
socket path long enough that the old suffix-based approach would exceed
the 104-byte limit, and verifies that the VM starts and achieves network
connectivity through gvproxy.
Assisted-by: Claude Code (Sonnet 4.6)
Signed-off-by: arewm <arewm@users.noreply.github.com>
whp: add Hyper-V enlightenment support and invariant TSC
Configure the WHP partition to expose Hyper-V enlightenments to the
guest, enabling it to use synthetic MSRs for timekeeping and
hypercalls instead of expensive VM exits on native hardware interfaces.
- Advertise Hyper-V CPUID leaves 0x40000000–0x40000006 (signature,
interface ID, version, feature identification, recommendations,
implementation limits, and hardware features).
- Enable synthetic processor features: HypervisorPresent, Hv1,
reference TSC, reference counter, frequency registers, hypercalls,
and VP index/runtime access.
- Set the MSR exit bitmap so unhandled MSR accesses trigger a VM exit
rather than injecting #GP into the guest.
- Retrieve and re-apply processor features banks with the invariant
TSC bit set, and expose it via CPUID 0x80000007.
- Register CPUID leaf 1 in the exit list so the hypervisor-present
bit (ECX.31) can be injected, which is required for the guest to
query the 0x40000000+ range.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
tests/virtiofs-misc: add mid-iteration duplicate detection subtest
Add a test that creates files while iterating a directory via readdir
and asserts no duplicate entries are returned. The test inserts entries
likely to shift iteration order, maximizing the chance of exposing
duplicates.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests/virtiofs-misc: add DirStream cache coherence subtests
Add 7 subtests that exercise readdir visibility after directory
mutations on an already-open handle: create, unlink, mkdir/rmdir,
symlink, link, rename (same-dir), and rename (cross-dir). Each test
opens a directory, reads entries to populate the cache, mutates the
directory, calls rewinddir, and asserts the mutation is visible.
These reproduce the macOS virtiofs DirStream cache coherence bug
fixed in PR #693.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests/virtiofs-misc: run subtests with catch_unwind
Wrap each subtest in catch_unwind so that a failure in one subtest
does not prevent the remaining subtests from running. The output now
lists each subtest as PASS/FAIL individually.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
balloon: use MADV_FREE to reclaim memory on macOS
The MADV_DONTNEED flag is no-op on macOS because it preserves the data,
so the OS cannot discard the pages. MADV_FREE flag actually frees
the memory under pressure, which is what we need for balloon.
Signed-off-by: Vadzim Dambrouski <pftbest@gmail.com>
init/dhcp: only overwrite resolv.conf with DNS
Only overwrite resolv.conf when the DHCP server has provide us some
nameservers. This allows users to keep the original resolv.conf in case
it was populated beforehand.
Signed-off-by: Sergio Lopez <slp@redhat.com>
arch: implement x86_64 Windows/WHP backend
Add the WHP-specific register, segment, and MSR configuration in
src/arch/src/x86_64/windows/. Uses structured WHV_REGISTER_VALUE writes
for FP registers and proper WHP register name mapping for MSRs. Includes
the get_page_size helper for Windows.
Signed-off-by: lstocchi <lstocchi@redhat.com>
arch/x86_64: infrastructure for Windows/WHP
Introduce src/arch/src/x86_64/linux/ and windows/ folder structures.
Move existing Linux/KVM code to linux/ and add platform re-exports
with cfg(target_os) conditional compilation.
Signed-off-by: Sergio Lopez <slp@redhat.com>
Signed-off-by: lstocchi <lstocchi@redhat.com>
arch/x86_64: decouple register computation
Move the logic that calculates GDT/IDT and page tables into
compute_segments() and compute_page_tables() functions that return
hypervisor-agnostic BootSegments and BootPageTables structs. Existing
KVM setup functions now delegate to these.
Signed-off-by: lstocchi <lstocchi@redhat.com>
arch/x86_64: abstract segment handling
Introduce a hypervisor-agnostic SegmentDescriptor struct and rename
kvm_segment_from_gdt to segment_from_gdt. The KVM backend uses a
kvm_segment_from conversion helper, proving the agnostic struct works
before adding the Windows path.
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/fs: report actual bytes written in used ring len
The virtio used ring's len field must reflect the number of bytes the
device wrote into the writable descriptor chain. libkrun was hard-coding
0, which broke FUSE protocol validation added in Linux kernel commit
68b69fa0edb (virtiofs: add FUSE protocol validation).
Resolves: https://github.com/containers/libkrun/issues/695
Signed-off-by: Gris Ge <cnfourt@gmail.com>
style: apply cargo fmt after 2024 edition migration
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
tests: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
krun-sys: migrate to Rust 2024 edition, bump rust-version to 1.85
Add [workspace] to mark it as a standalone crate (not a workspace
member), bump edition to 2024 and rust-version to 1.85.0. No source
changes were required by cargo fix.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
libkrun: migrate to Rust 2024 edition
- Bump edition from 2021 to 2024 in Cargo.toml
- Wrap #[no_mangle] with #[unsafe(no_mangle)] (84 occurrences)
- Add unsafe {} blocks inside unsafe fn bodies (unsafe-op-in-unsafe-fn)
- Collapse nested if-let blocks flagged by clippy::collapsible_if
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
aws_nitro,whp: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
vmm: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
rutabaga_gfx: fix virgl_renderer unsafe issues missed in 2024 migration
The virgl_renderer feature requires system libraries not available during
the initial migration, so these code paths were not reached by cargo fix.
Also fix two collapsible_if clippy warnings in virtio/gpu that only
surface when compiling krun-devices with the gpu feature.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
devices: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
rutabaga_gfx: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
hvf: migrate to Rust 2024 edition
- Add `unsafe` to all 79 extern "C" blocks (78 in bindings.rs, 1 in lib.rs)
- Remove redundant `ref` binding modifier in match arm (2024 edition error)
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
kernel: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
arch: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
cpuid: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
arch_gen,utils,smbios,polly: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
tests: consistently use `get_krun_add_net_unixgram`, don't duplicate the definition
Signed-off-by: Jan Noha <nohajc@gmail.com>
update `integration_tests.yml` to run FreeBSD tests
- have to install lld and also clang on the self-hosted runner
Signed-off-by: Jan Noha <nohajc@gmail.com>
tests: add FreeBSD guest support
- simple boot test, gvproxy connect and listen tests
- fix timing issue in all network listen tests
- replace the macOS-only `cfg` gate with a `gvproxy -version` check
Signed-off-by: Jan Noha <nohajc@gmail.com>
arch/vmm: add PVH boot protocol support for FreeBSD x86_64
Implement PVH (Para-Virtualized Hardware) boot protocol support,
enabling FreeBSD x86_64 kernels to boot via the Xen PVH ABI.
Key changes:
- arch/Cargo.toml: add linux-loader (elf feature) as x86_64 dep
- layout.rs: add PVH_INFO_START, MODLIST_START, MEMMAP_START, RSDP_ADDR
- mptable.rs: export MPTABLE_START as pub
- gdt.rs: fix get_limit() to apply granularity (G) bit expansion
- mod.rs: add configure_pvh(), add_memmap_entry(); refactor
configure_system() to dispatch to configure_pvh() or
configure_64bit_boot() based on pvh flag; add pvh param
- regs.rs: add pvh param to setup_regs/setup_sregs/
configure_segments_and_sregs; set 32-bit protected mode GDT and
CR0=PE|ET for PVH; set rbx=PVH_INFO_START per PVH ABI
- vstate.rs: thread pvh through configure_x86_64()
- builder.rs: detect PvhEntryPresent in ELF load result; propagate
pvh through PayloadConfig, load_payload, create_vcpus_x86_64
- lib.rs: add pvh param to Vmm::configure_system()
Co-authored-by: Dmitrii Sharshakov <d3dx12.xx@gmail.com>
Assisted-by: Copilot:claude-opus-4.6
Signed-off-by: Jan Noha <nohajc@gmail.com>
CI: Check for improper AI atribution
Add a script to enforce rules mentioned in CONTRIBUTING.md.
Main motivation for this is banning Co-Authored-By: Claude.
This also checks for other ill-formated Assisted-By forms and generates a
warning.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
CONTRIBUTING.md: Update Assisted-by/Generated-by guidlines
We previously only allowed using:
Assisted-by: Claude Code:claude-opus-4.6
Update the guidlines to also other well formated alternatives:
Assisted-by: Claude Code / claude-opus-4.6
Assisted-by: Copilot (GPT-5.4)
Also allows a similar Generated-By tag:
Generated-by: OpenCode:claude-opus-4.6
This also explicitly codifies you can use `auto` or `unknown` in case you don't
know the actual model name. This can be used when you are using automatic
selection in tools like Cursor or Github Copilot.
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init: replace dummy IP address
In commit 2593accd9b, we extended init to configure a dummy interface
when TSI is enabled, to help local applications find out the VM has
network connectivity.
For the dummy interface, we've used the 10.0.0.1/8 address, which
collides with the network ranges used by podman by default. In
principle, this shouldn't be a problem since, with TSI hijack enabled,
the kernel takes over the application's INET socket.
But, to implement both the INET and VSOCK personalities, when connect()
is called on a TSI socket, TSI attempts to connect to the endpoint first
with the INET personality, falling back to VSOCK only if that one fails.
For SOCK_DGRAM sockets, a connect() via the INET personality will always
succeed if there's a route defined that would allow to reach the
destination address.
So, with a dummy inteface configured with a 10.0.0.1/8 address,
attempting to access a DNS server within that range leads to the
following flow:
- Userspace creates the AF_INET/SOCK_DGRAM socket.
- The kernel hijacks the AF_INET returning an AF_TSI socket instead.
- Userspace calls connect() on the socket.
- tsi_connect() attemps connecting via the INET personality first, with
success.
- For TSI, the socket becomes effectively an AF_INET socket, not
allowing external communitations via VSOCK.
To avoid this issue, let's switch to an address from IANA's TEST-NET-3
block, which shouldn't be used by any network external to the VM.
Signed-off-by: Sergio Lopez <slp@redhat.com>
virtio/fs: inline do_lookup() into lookup()
do_lookup() originally existed to hold the init.krun special-case
logic separate from the trait method. Now that AugmentFs handles
init.krun, do_lookup() has no special cases and can be inlined.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
CI: enable BLK=1 and install e2fsprogs in integration tests
Build and test with the block device feature so the root-disk-remount
test runs in CI. Install e2fsprogs (provides mke2fs) which the test
needs to create the ext4 disk image.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests: add root-disk-remount integration test
Boot from an ext4 block device via krun_set_root_disk_remount. The
virtiofs root uses NullFs with init.krun and virtual mount-point
directories overlaid. The guest verifies it pivoted to the block
device root successfully.
Uses dlsym for krun_add_disk/krun_set_root_disk_remount so the test
compiles without BLK and skips gracefully at runtime.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests: add augmentfs integration test
Boot a VM with a pure NullFs root — no host directory at all. Every
file in the root (init.krun, guest-agent, .krun_config.json, test
data) is injected as a virtual overlay, and /dev, /proc, /sys are
virtual empty directories used as mount points.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: move EXIT_CODE ioctl to AugmentFs
The exit-code ioctl is a krun mechanism, not a filesystem operation.
Move it to the AugmentFs overlay where it is handled before any
delegation to the inner filesystem.
The Linux passthrough retains only EXPORT_FD (which needs access to
passthrough-internal handle and export tables). The macOS passthrough
no longer implements ioctl at all.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: remove REMOVE_ROOT_DIR ioctl and allow_root_dir_delete
The temporary root directory hack is gone (replaced by NullFs), so
the ioctl that cleaned it up and the config flag that gated it are
no longer needed. Remove allow_root_dir_delete from FsDeviceConfig,
Fs::new(), passthrough Config, and all call sites.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: rewrite krun_set_root_disk_remount to use NullFs
krun_set_root_disk_remount no longer creates a temporary empty host
directory. Instead it configures a NullFs-backed virtiofs device
(shared_dir: None) with init.krun overlaid via AugmentFs.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: add NullFs, a minimal empty-root FileSystem
NullFs implements the FileSystem trait with just an empty root
directory. It can be wrapped with AugmentFs to serve virtual
files without any host directory involvement.
Fs::new() now accepts Option<String> for shared_dir — None selects
NullFs. FsDeviceConfig and FsServer gain the corresponding variants.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
libkrun.h: document that implicit resource creation will become opt-in
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: add krun_get_default_init()
Add API to retrieve the built-in default init binary. Callers that
use krun_disable_implicit_init() can use this to obtain the init
binary and inject it themselves via krun_fs_add_overlay_file().
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: add krun_fs_add_overlay_file(), krun_fs_add_overlay_dir() APIs
Add C APIs to inject virtual files and directories into a virtiofs
device. Entries are backed entirely by host memory (no host file).
Files support one-shot semantics (disappear after the first lookup).
Paths may contain '/' to nest entries inside existing virtual
directories (e.g. krun_fs_add_overlay_dir for "etc", then
krun_fs_add_overlay_file for "etc/hostname"). Intermediate
directories must already exist; -ENOENT / -ENOTDIR is returned
otherwise.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: add krun_disable_implicit_init()
Add API to prevent the default init binary (/init.krun) from being
injected into the root filesystem. Follows the existing
krun_disable_implicit_{console,vsock} pattern.
Must be called before krun_set_root().
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: introduce generic AugmentFs overlay for files like init.krun
Introduce AugmentFs<T>, a generic overlay that wraps any FileSystem
implementation and intercepts FUSE operations for virtual inodes —
synthetic read-only files and directories backed by static data.
One-shot files can only be looked up once.
Remove all init.krun special-case code (init_inode, init_handle,
INIT_CSTR) from both the Linux and macOS passthrough implementations.
The init.krun virtual file is now configured via VirtualDirEntry in
the krun API layer and handled generically by the overlay.
FsDeviceConfig carries a Vec<VirtualDirEntry> and FsWorker wraps
AugmentFs<PassthroughFs> / AugmentFs<PassthroughFsRo>.
The InodeAllocator now starts at ROOT_ID + 1 since the init_inode
reservation is no longer needed.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: introduce InodeAllocator for shared inode numbering
Replace the private next_inode AtomicU64 inside PassthroughFs with a
shared InodeAllocator that is passed in at construction. This lets
multiple layers (e.g. a future virtual-inode overlay) allocate from
the same counter without implicit coordination via reserved ranges.
The allocator starts at ROOT_ID + 2, reserving inode 2 for the
existing init_inode in PassthroughFs. This reservation is removed
in the next commit when init handling moves to AugmentFs.
PassthroughFs::new() and PassthroughFsRo::new() now take an
Arc<InodeAllocator> parameter. FsWorker::new() creates the allocator
and passes it through.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: extract init binary blob into its own crate
Move the init binary build script and include_bytes!() from the
devices crate into a new init-blob crate. The passthrough modules
reference the binary as init_blob::INIT_BINARY instead of using
include_bytes! directly.
Inspired by https://github.com/containers/libkrun/pull/593.
Suggested-by: Geoffrey Goodman <geoff@goodman.dev>
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
Add AGENTS.md and .claude
Adds an AGENTS.md file to give agents context about libkrun. This
includes project structure and coding standards.
Create a .claude/ directory that points the Claude Code agent to the AGENTS.md file since
that file isn't supported.
This was initially generated by running /init inside of Claude Code. I
took some sections from
https://github.com/multica-ai/andrej-karpathy-skills/blob/main/CLAUDE.md?plain=1
and
https://github.com/openai/openai-agents-python/blob/f20aa40f56ec374841dfe6e4f3d6118c8e32fb2b/AGENTS.md.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init/aws-nitro: Continue if module already loaded
If a module is already loaded, then the initramfs can continue on
loading other modules. Although returning an error, this does not
require failure of the initramfs.
Signed-off-by: Tyler Fanelli <tfanelli@redhat.com>
devices/vsock: support multi-descriptor TX chains
from_tx_virtq_head assumed a fixed 2-descriptor layout (header + one
data descriptor), which breaks with newer kernels that may combine
header and data in a single descriptor (Linux 6.2+) or split data
across multiple descriptors.
Handle three cases: single combined descriptor (zero-copy), classic
two-descriptor (zero-copy, unchanged), and multi-descriptor data
(copied into an owned contiguous buffer). The RX path already handled
the combined case; this brings the TX path to parity and beyond.
Assisted-by: Claude Code:claude-opus-4.6
Signed-off-by: Sergio Lopez <slp@redhat.com>
init: fix unescape_string() not advancing past escape characters
The unescape_string() function in init.c, which handles JSON escape
sequences when parsing environment variables from .krun_config.json,
had a bug where the pointer 'val' was not advanced past the escape
character after processing it.
When encountering a two-character JSON escape sequence like \n or \",
the switch statement pre-increments val to point at the escape
character (e.g., 'n' or '"') and writes the unescaped byte to the
output. However, it never advances val past that character. On the
next loop iteration, the character is not a backslash, so it gets
copied again as a literal character.
This causes:
- \n (JSON-escaped newline) to produce a newline followed by a
literal 'n'
- \" (JSON-escaped double quote) to produce two double quotes
For example, an environment variable set to a JSON string like:
{\"key\": \"value\"}
would be rendered inside the krun VM as:
{""key"": ""value""}
Fix this by adding val++ after writing the unescaped character in
each case of the switch statement. The 'u' (unicode) case already
handles its own pointer arithmetic and is not affected.
Fixes: https://github.com/containers/libkrun/issues/678
Assisted-by: <anthropic/claude-opus-4.6>
Signed-off-by: Dusty Mabe <dusty@dustymabe.com>
README: add security email section
Add a section telling users to report security issues to
libkrun-security@redhat.com instead of the GH issue tracker or Matrix.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
README.md: link to CONTRIBUTING.md
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
Add contribution guidelines
Codify existing conventions for commits, DCO sign-off, rebase merging,
and add a policy for AI-assisted contributions (inspired by the Linux
kernel's).
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
rutabaga_gfx/cross_domain: fix audio glitches by dropping write lock
The previous write() held item_state.lock() across the write_volatile call
that performs the actual syscall, serializing every cross-domain CMD_WRITE
behind any other operation on the items table — including add_item from
process_receive, which fires for every new fd received via SCM_RIGHTS as
guests open additional channels.
For per-period eventfd wakeups (e.g. PipeWire audio streams signaling host
playback every audio period), this means the write completes only after any
in-flight item_state operation finishes. Under stream-create churn — many
guest applications opening streams concurrently, each delivering new fds via
SCM_RIGHTS that hit add_item under the same lock — the wait can exceed the
audio period budget and produce missed-deadline glitches at the host's
audio output.
This change shrinks the critical section to a brief fd dup() under the lock,
performs the syscall lock-free, and only re-acquires the lock if hang_up
indicates the item should be removed. In the common case (hang_up == 0,
e.g. repeated eventfd wakeups for an active stream) the table is no longer
touched per write, eliminating both the contention and the previous
"remove + conditional re-insert" churn.
Behavioral changes vs the old code:
- Common case (hang_up == 0): item stays in the table; we hand out a dup'd
fd for the write. Net behavior identical, lock hold time bounded by dup().
- hang_up == 1: item removed in a separate phase after the write, instead
of "removed unconditionally then re-inserted on hang_up == 0". Same
observed end state.
- Concurrent writes to the same id (no longer serialized): the kernel
guarantees atomicity for eventfd writes (8 bytes) and pipe writes
<= PIPE_BUF, which are the only two CrossDomainItem variants this branch
handles. Each caller dup's its own fd and writes through it independently.
Verified with a synthetic reproducer: a sustained 1 kHz sine playing through
a guest PipeWire stream while ~200 short-lived guest streams open and close
concurrently (each issuing SCM_RIGHTS for new eventfds, contending with the
sustained stream's per-period writes for item_state). Capturing the host
sink monitor and counting sample-to-sample deltas exceeding a clean-sine
threshold, the stress workload produced ~10 distinct glitch bursts in an
8-second capture before this change, and zero across five consecutive runs
after.
Signed-off-by: Adam Ford <adam.ford@anodize.com>
whp: implement safe wrappers for VM, vCPU, and instruction emulation
This commit introduces the foundational Rust abstractions for the Windows
Hypervisor Platform (WHP) backend. It encapsulates the raw C FFI bindings
into safe, idiomatic Rust structs (`WhpVm`, `WhpVcpu`, and `WhpEmulator`).
Key features include:
- Partition & vCPU Management: Safe wrappers for partition configuration,
CPUID masking, memory mapping, and `WHvRunVirtualProcessor` exit routing.
- High-Performance Register I/O: Introduces const-generic helpers
(`get_registers<N>` and `set_registers<N>`) to guarantee stack-allocated,
zero-overhead register reads/writes on the VM-exit hot path.
- Robust TSC Calibration: Implements accurate host TSC frequency detection
via CPUID 0x15/0x16, with a reliable `QueryPerformanceCounter` (QPC)
calibration fallback for AMD/older hardware.
- Emulation: Integrates WHP's built-in x86 emulator for MMIO and PIO handling.
Signed-off-by: lstocchi <lstocchi@redhat.com>
Remove builtin virtio-snd device
The builtin virtio-snd device is not actively maintained or tested and
has been superseded by vhost-user-snd. Remove it as part of the libkrun
2.0 breaking changes.
This removes:
- The virtio-snd device implementation (src/devices/src/virtio/snd/)
- The 'snd' feature flag from all crates
- The pipewire (pw) dependency from devices
- The krun_set_snd_device() C API and KRUN_FEATURE_SND constant
- CI references to the snd feature and libpipewire-0.3-dev
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init: Set up a dummy network interface with TSI
Some applications check for network availability by looking for a
network device configured for Internet access. When TSI is used, there
is no such device available by default, although Internet is accessible.
Then those applications behave like when the connection is not
available.
Let's solve this problem by setting up a dummy network interface. The
dummy interface is automatically created when CONFIG_DUMMY is enabled in
kernel or the corresponding kernel module is loaded. This means a
sufficiently recent libkrunfw version is needed (see
https://github.com/containers/libkrunfw/pull/116). The dummy interface
is initially down.
In order to make the applications happy, the interface must be brought
up and set up for Internet connections. This is ensured by setting the
IP address to 10.0.0.1/8 (an arbitrary choice without any special
reason) in init.c if TSI is enabled. The netmask is selected to be
sane; it doesn't cover the whole IP range and we cannot set a default
route because then TSI has problems, but it's OK for the tested
application. We can change it if some application has trouble with
that.
TSI availability is determined by checking the presence of `tsi_hijack'
in the kernel command line, before `--' delimiter if present.
The dummy interface simply swallows all packets. But it is effectively
bypassed by TSI for practical purposes. Things like ICMP don't work in
either case.
When the kernel support is not available, the device is not present and
init.c cannot set it up. We skip the configuration silently in such a
case, to not spam users with errors if they use older libkrunfw or
custom kernels.
Fixes: #576
Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
rutabaga_gfx/cross_domain: handle CMD_WRITE on Eventfd items
The cross_domain `write` handler only matched
`CrossDomainItem::WaylandWritePipe`, falling into the catch-all for
every other item type after the unconditional `remove()` at the top of
the function had already dropped the entry from the table. PipeWire
(and other clients that share host-created eventfds via SCM_RIGHTS for
per-period wakeups) sends CMD_WRITE on those eventfd identifiers — the
first such write returns InvalidCrossDomainItemType *after* removing
the item, and every subsequent write on the same identifier returns
InvalidCrossDomainItemId, masquerading on the guest as the opaque
VIRTIO_GPU_RESP_ERR_UNSPEC (0x1200).
Reproduced inside a libkrun guest on x86_64 by routing PipeWire's
ALSA-shim audio through a host PipeWire daemon. With a paired BT
speaker as the sink, `speaker-test -D pipewire -c2 -t wav -l 1`
produces, per stream, ~10 entries of:
[ 0.682819] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 0.723762] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 0.767615] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 0.807779] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 0.852469] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 0.896552] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 0.936504] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 0.980567] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 1.024476] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
[ 1.064636] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207)
with audio still playing — PipeWire has socket-based fallback timing
that doesn't depend on eventfd ack, so the failures are cosmetic for
playback. They are not cosmetic for clients that strictly require the
eventfd handshake (PipeWire's ALSA shim under heavier loads, and the
buffer-pool wakeup path used by V4L2 capture streams).
Add an Eventfd arm that mirrors the WaylandWritePipe semantics:
`write_volatile` performs the 8-byte counter increment, and the item
is re-inserted into the table unless the guest signaled `hang_up`.
Verified post-fix: zero CMD_WRITE failures, zero `0x1200` entries in
guest dmesg, audio playback unchanged. Camera capture (gst-launch
pipewiresrc → MJPEG) also exercises this path for buffer-pool wakeups
and runs cleanly with valid 98%-non-zero JPEG frames.
Signed-off-by: Adam Ford <adam.ford@anodize.com>
init: fix FreeBSD build by guarding tmpfs mount with __linux__
The tmpfs mount added in 536360aa uses Linux-specific MS_* flags and
the Linux mount() signature, which don't exist on FreeBSD. Guard the
block with #if __linux__ to fix the FreeBSD cross-compilation.
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests: add virtiofs-misc test for fallocate operations
Tes…
The integration test configure-vm-2cpu-1GiB was failing because the
guest kernel ran out of memory trying to create workqueue worker threads
during boot ([ 2.597935] workqueue: Failed to create a worker thread:
-12). The previous ENOMEM workaround (an unconditional 5 ms sleep
before every kvm_run) was designed to prevent kvm_run itself from
returning ENOMEM; it did not prevent the host from failing to back a
guest page allocation that the guest kernel triggered at runtime.
Two complementary fixes:
Pre-fault guest RAM (builder.rs)
---------------------------------
After create_guest_memory() assembles the GuestMemoryMmap, iterate every
region and call madvise(MADV_POPULATE_WRITE) to force the host kernel to
commit physical pages before any vCPU starts running. This converts a
silent mid-boot allocation failure into a clean, early "not enough host
memory" error at VM creation time. On kernels older than 5.14 that do
not support MADV_POPULATE_WRITE, fall back to a manual page-touch loop
(write one byte per 4 KiB page). Both paths are best-effort: if the
host genuinely cannot satisfy the request, the VM continues and the
reactive backoff below handles individual faults.
Reactive ENOMEM backoff in kvm_run (vstate.rs)
-----------------------------------------------
Remove the unconditional 5 ms pre-sleep, which penalised every vCPU
exit even when the host had ample memory. Replace it with a reactive
exponential back-off: when kvm_run itself returns ENOMEM (the original
bug from issue #314 on kernels 6.12/6.13), sleep for an increasing
duration (starting at 5 ms, doubling on each consecutive failure, capped
at 100 ms) and return VcpuEmulation::Handled to retry, instead of
crashing the VM. The back-off decays by 1 ms on every successful run,
returning to zero quickly once memory pressure subsides. This path is
only active when KRUN_ENOMEM_WORKAROUND is set.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code: claude-sonnet-4-6
virtio/console: fix TX data loss race on shutdown
The 'Integration Tests (Linux x86_64)' CI job has been failing on all
recent PRs. The most basic test, configure-vm-2cpu-1GiB, produces an
empty stdout.txt despite the VM booting cleanly and the guest-agent
running successfully. The diagnostic log shows:
TRACE process_tx] tx unparked, queue len 0
INFO krun_vmm] Vmm is stopping.
The TX thread woke up, saw an empty queue, and exited — losing the
guest's final println!("OK") output.
Root cause: race in pop_head_blocking
--------------------------------------
The TX thread parks via thread::park() when the queue is empty and is
woken either by notify_tx() (from the event-manager thread, which fires
when the guest writes the virtio queue notification eventfd) or by
port::shutdown() (which sets stop=true then calls unpark()).
The old code:
loop {
match queue.pop(mem) {
Some(d) => break Some(d),
None => {
interrupt.signal_used_queue();
if stop.load(Acquire) { break None; } // exits here
thread::park();
}
}
}
The race window:
TX thread Shutdown thread Event-manager thread
─────────────────────────────────────────────────────────────────────
queue.pop() -> None
interrupt.signal_used_queue()
stop.load() -> false stop.store(true)
thread::park() tx_thread.unpark()
<- wakes
queue.pop() -> None (guest wrote "OK\n"; notification in flight)
stop.load() -> true
break None <- EXITS notify_tx() -> unpark()
[TX thread already gone]
The guest's final bytes are in the virtio avail ring when the TX thread
exits. The event-manager's notify_tx() arrives after termination. The
stdout.txt file is empty; check() returns TestOutcome::Fail.
Why only x86_64?
-----------------
The aarch64 self-hosted runner sets KRUN_NO_UNSHARE=1, skipping the
buildah-unshare + network-namespace wrapper. Without that wrapper the
per-test process lifetime is different (longer VM teardown path), making
the race window substantially narrower. It does not surface in practice
on aarch64.
Fix: drain the queue with the disable/enable_notification fence
---------------------------------------------------------------
The correct pattern — already used by the block and net device workers
(block/worker.rs, net/worker.rs) — is to suppress guest notifications
while draining, then call enable_notification() which does a SeqCst
fence and re-reads avail_idx. If new entries arrived while
notifications were suppressed, enable_notification returns true and we
drain again. Only after the fence confirms the queue is empty do we
check stop. If stop is true at that point, no bytes remain unprocessed
and we can return None safely.
The stop check is deliberately placed after the drain+fence: if
stop was set while the guest was writing its last bytes, we flush them
before exiting.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code: claude-sonnet-4-6
AGENTS.md: be explicit about incorrect Agent Attribution format
Be explicit in the AGENTS.md file about not using the Co-authored-by:
attribution format.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
.claude: fix path to AGENTS.md file
Based on the memory documentaiton, @AGENTS.md imports are resolved
relative to the file containing the import, not the working directory.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
vmm: fix riscv64 non-TEE memory region setup
In non-x86 builds that don't use TEE features, vstate.rs used cfg!() to
distinguish between TEE and non-TEE memory setup paths.
cfg!() only evaluates to a compile-time constant; code in both branches
still participates in compilation and type checking. As a result, normal
riscv64 builds still type-check guest_memfd and memory attribute code,
which leads to build failures.
Refactor the memory-region setup into cfg-gated helpers so non-TEE builds
do not compile TEE-only memory setup code, and reject unsupported
TEE+architecture combinations during VM setup instead of during memory
initialization.
Verify with:
cargo check -p vmm --target riscv64gc-unknown-linux-gnu
Signed-off-by: Zewei Yang <yangzewei@loongson.cn>
virtio-fs: add a regression test for scoped_cred restore
Drive the real ScopedUid through repeated per-request switches on a
thread set up like an unprivileged server holding CAP_SETUID (the
state that exposed the hardcoded euid-0 restore), and assert the
thread is left back at the server uid rather than wedged at the guest
uid. Credential changes are per-thread, so the test runs on a
dedicated thread and leaves the rest of the binary untouched.
It needs CAP_SETUID and a non-zero uid to switch to, so it runs under
the suite's buildah-unshare userns (or as root) and skips otherwise.
Against the pre-fix Drop it fails with the worker stuck at the guest
uid; with the fix it passes.
Signed-off-by: Hans Jørgen Hoel <hansjorg@urkilden.no>
Assisted-by: Claude Code: claude-fable-5
virtio-fs: restore the original thread credentials, not euid/egid 0
scoped_cred's Drop resets the worker thread's effective uid/gid to a
hardcoded 0, assuming the server runs as root. That breaks an
unprivileged server granted CAP_SETUID/CAP_SETGID (e.g. via systemd's
AmbientCapabilities=): set_creds sees the capability and switches, but
restoring to 0 poisons the thread. A first switched request's Drop parks
the thread at euid 0; the next switch to a non-zero uid clears the
thread's effective capabilities (capabilities(7), "Effect of user ID
changes on capabilities"); that request's Drop then fails EPERM, and the
thread is stuck with the guest uid's credentials. Every later request it
handles -- including ones for guest root -- fails EACCES/EPERM on host
files that uid cannot reach.
Any guest issuing requests as more than one uid wedges within a few ops.
A Debian guest running 'apt-get update' (whose fetch methods drop to the
_apt user) reproduces it: the InRelease/Packages renames fail with
'rename failed, Permission denied'.
Capture the effective id before switching and restore that on drop. A
root-run server captures 0 and is unchanged; an unprivileged server now
only switches between non-zero uids, never clearing its caps -- and the
worker thread no longer escalates itself to host euid 0.
Signed-off-by: Hans Jørgen Hoel <hansjorg@urkilden.no>
Assisted-by: Claude Code: claude-fable-5
lib: remove deprecated krun_set_root
Callers should use krun_add_virtiofs3() with KRUN_FS_ROOT_TAG
("/dev/root") instead, which provides explicit control over DAX
window size and read-only mode.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: replace krun_set_root with krun_add_virtiofs3
Replace all callers of krun_set_root with explicit
krun_add_virtiofs3(ctx, "/dev/root", path, 0, false) calls.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
include: remove stale implicit resource creation comment
All krun_disable_implicit_* functions are gone. The 2.0 API requires
explicit resource creation.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove krun_disable_implicit_vsock and implicit vsock creation
Vsock creation is now fully explicit via krun_add_vsock(). No vsock
device is created unless the caller requests one.
Remove the Implicit variant from VsockConfig, the implicit vsock
creation heuristics in krun_start_enter, and krun_disable_implicit_vsock.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: use explicit krun_add_vsock everywhere
- test_tsi_tcp_guest_connect: add krun_add_vsock(ctx, KRUN_TSI_HIJACK_INET)
- test_tsi_tcp_guest_listen: same
- test_vsock_guest_connect: add krun_add_vsock(ctx, 0)
- chroot_vm.c: replace krun_disable_implicit_vsock + vhost-user
with explicit krun_add_vsock when not using vhost-user-vsock
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove krun_disable_implicit_console, krun_set_console_output, and implicit console
Console creation is now fully explicit via krun_add_virtio_console_default
or krun_add_virtio_console_multiport. No console is created unless the
caller requests one.
Remove the disable_implicit_console field from VmResources, the implicit
console and serial device creation paths in builder.rs, the console_output
field and setter on VmResources, and krun_set_console_output (kept only
behind cfg(aws-nitro) where NitroEnclave still needs it).
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: use explicit krun_add_virtio_console_default everywhere
Replace implicit console creation with explicit
krun_add_virtio_console_default calls. Also replace
krun_set_console_output in nitro.c with
krun_add_virtio_console_default.
No test or example relies on implicit console injection anymore.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove unsupported krun_set_mapped_volumes
This function has been returning -EINVAL unconditionally. Remove it.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove deprecated krun_set_log_level
Superseded by krun_init_log which provides control over target fd,
log style, and env override options.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: propagate KRUN_NITRO_DEBUG flag in krun_init_log
krun_set_log_level set KRUN_NITRO_DEBUG when level==4 (debug), but
krun_init_log did not. Fix the omission so removing krun_set_log_level
doesn't regress nitro debug logging.
Also fix the condition to level >= 4 so that trace (level 5) also
enables nitro debug.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests+examples: replace krun_set_log_level with krun_init_log
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove deprecated krun_set_passt_fd, krun_set_gvproxy_path, krun_set_net_mac
These were replaced by krun_add_net_unixstream, krun_add_net_unixgram,
and krun_add_net_tap (which take mac as a parameter directly).
Also remove the internal LegacyNetworkConfig enum, legacy_net_cfg and
legacy_mac fields, the compat path in krun_start_enter that converted
them to the new net backend, and the now-unused NET_COMPAT_FEATURES
constant.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: remove deprecated krun_set_root_disk and krun_set_data_disk
These were replaced by krun_add_disk. Also remove the internal
root_block_cfg/data_block_cfg fields and their setters, and simplify
get_block_cfg() now that the legacy compat path is gone.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
examples: replace deprecated krun_set_root_disk/krun_set_data_disk with krun_add_disk
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init: remove init.c
Remove the init.c file since we are now using the rust implementation.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: loosen perms on /dev/kvm if it exists
Best effort loosen /dev/kvm permissions to allow nested virtualization
by unprivileged processes inside the microVM (usually a single purpose
environment). Log errors but don't log ENOENT since the guest kernel
may not support KVM or nested virtualization might not be enabled.
Port of containers/libkrun#708.
Assisted-by: Claude Code:opus-4.6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init/dhcp: only overwrite resolv.conf with DNS
Port of upstream commit 378e5249 ("init/dhcp: only overwrite
resolv.conf with DNS"). Only write /etc/resolv.conf when the DHCP
server provides nameservers, preserving any pre-existing content.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: match KRUN_INIT_PID1 by prefix rather than exact equality
The C init checked *env_init_pid1 == '1' (first-byte comparison),
accepting any value starting with '1' — including "10" or "1\n" (which
can appear when the value originates from a file read). The Rust port
used exact equality with "1", silently ignoring those variants.
Replace with is_ok_and(|v| v.starts_with('1')).
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: exit 125 when setup_redirects cannot open virtio-ports
The C init called exit(125) if setup_redirects() returned a negative
value (which it did when opendir("/sys/class/virtio-ports") failed).
The Rust port returned silently, letting the workload run with
unredirected stdio and no diagnostic.
Match the C behaviour: print an error and exit(125) so callers get a
visible signal that the redirects could not be set up.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: fork timesync child process instead of spawning a thread
A thread is destroyed when the parent calls execvp() (in PID1 mode).
The C init ran clock_worker() in a forked child process, which survives
exec. Match that behaviour: create the vsock socket, fork, and run the
recv loop in the child; the parent closes its copy of the socket and
returns immediately.
Also switch to nix wrappers throughout: socket::socket(), socket::recv(),
time::clock_gettime(), and time::clock_settime() replace the equivalent
unsafe libc calls. Add the nix "time" feature to support the clock
functions.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: document argv fallback divergence from C init
The C init replaced argv[0] with "/bin/sh" when neither KRUN_INIT nor
a config file was present, forwarding remaining cmdline tokens as shell
arguments. The Rust init instead treats proc_args[1] as the executable
directly.
Add a comment explaining the rationale: callers that omit both KRUN_INIT
and a config file intend the cmdline argument to be the command, not a
shell script path, making the Rust behaviour more intuitive.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: accept colon-separated rlimits and strip outer quotes
Two issues with apply_rlimits():
1. The C init parsed KRUN_RLIMITS with strtoull() and a single-char skip,
so any separator character between ID, CUR, and MAX worked (e.g.
"7:1024:4096"). The Rust code required "ID=CUR:MAX" and silently
skipped entries using the historical colon-only format.
2. krun_set_rlimits() wraps the entire value in double-quotes
(format!("\"{}\"", ...)), so the env var received by init is
"\"7=1024:4096\"". Neither the old Rust nor the C parser handled
this correctly.
Fix both by extracting parse_rlimit_entry() which strips outer '"' chars
and splits on the first two occurrences of '=' or ':' via splitn(3).
Both formats and the quoted form now parse correctly.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: add Entrypoint field and Docker-format config key aliases
The C init accepted "Cmd", "Env", "WorkingDir"/"Cwd", and "Entrypoint"
keys via case-insensitive comparison; the Rust port only handled OCI
runtime-spec keys ("args", "env", "cwd" inside "process").
Add serde aliases so RawConfig's flat fields also accept the Docker image
config capitalisation:
- "Cmd" aliases "args"
- "Env" aliases "env"
- "WorkingDir"/"Cwd" alias "cwd"
- new "Entrypoint" field (top-level, Docker format only)
When Entrypoint is present it is prepended to the resolved args vector,
matching the C init's concat_entrypoint_argv() behaviour.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: bring up loopback interface on FreeBSD
The non-Linux setup_network() stub was empty, so the lo interface was
never raised inside FreeBSD guests. The C init unconditionally brought up
lo on all platforms (the #if __linux__ guard covered only the DHCP block,
not lo setup).
Use nix::sys::socket to open an AF_INET/SOCK_DGRAM socket and issue
SIOCSIFFLAGS / IFF_UP, matching the C behaviour.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: set up a dummy network interface with TSI
Port of 2593accd9b57da23912d1824216bcc53866c838d. When TSI is active,
brings up dummy0 and assigns it 10.0.0.1/8 so applications that probe
for network availability see a configured interface. Silently skips
setup if the dummy driver is absent in the kernel.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: remove KRUN_REMOVE_ROOT_DIR_IOCTL
Port of cd8b2be0af316fe4c38b6749a888c56ca7699277. The temporary root
directory hack has been replaced by NullFs, so the ioctl that cleaned
it up is no longer needed.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: remove C sources and tee/ attestation code
Delete init/init.c, init/dhcp.c, init/dhcp.h, init/jsmn.h, and the
entire init/tee/ directory (snp_attest.c/h and the KBS client).
The amd-sev feature no longer performs LUKS unlock or KBS attestation —
it mounts /dev/vda as ext4 like the tdx path does.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: port timesync clock_worker to Rust
Implements the timesync feature behind the `timesync` cargo feature flag.
Receives host-side nanosecond timestamps over AF_VSOCK/SOCK_DGRAM on port
123 and applies them via clock_settime when the delta exceeds 100ms.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
ci: fix FreeBSD cross-compilation
Replace the C-based BSD init build rule (which referenced the now-deleted
init/init.c) with a cargo build rule targeting the correct Rust triple.
Makefile:
- Remove dead INIT_SRC = init/init.c variable.
- Derive FREEBSD_RUST_TARGET from the host ARCH with arm64→aarch64
substitution to get the correct Rust triple.
- Set CARGO_BSD_RUSTFLAGS with the clang cross-linker flags (mirroring
the existing CC_BSD setup) so cargo can link for FreeBSD.
- aarch64-unknown-freebsd is a Tier 3 target with no prebuilt std;
use +nightly -Z build-std for that case.
setup-build-env:
- Add rustup target add x86_64-unknown-freebsd (Tier 2, prebuilt std).
- Install nightly toolchain + rust-src for the aarch64 FreeBSD case.
cross-compilation.yml:
- Add clang to the Linux cross-compilation dependencies so the
FreeBSD linker flags resolve correctly on Linux runners.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: wire FreeBSD platform support
Connect the FreeBSD helpers into the boot sequence:
- open_console() and populate_env_from_kenv() are called at the very
start of main() before anything else
- setsid/TIOCSCTTY are Linux-only; open_console() handles session setup
on FreeBSD
- setlogin("root") is called on FreeBSD after console setup
- KRUN_DHCP and DHCP setup are Linux-only
- If KRUN_CONFIG is not set, mount_config_iso() is attempted; the ISO
is unmounted immediately after config::load() returns
- fs::* mounts and mount_shared_root are Linux-only
- exec_workload() calls open_console() on FreeBSD instead of
setup_redirects(), giving the child process a fresh controlling
terminal before execvp
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: add FreeBSD platform helpers
Add init/src/freebsd.rs with:
- kenv_get(): reads a variable from the FreeBSD kernel environment via
kenv(2), which is the source of env vars for init before the process
environment is set up
- populate_env_from_kenv(): imports the known KRUN_* variables from
kenv into std::env at startup so the rest of the code can use
std::env::var uniformly on both platforms
- open_console(): replicates login_tty(3) without linking libutil —
revokes existing opens of /dev/console, opens it, creates a new
session via setsid(2), sets the controlling terminal via TIOCSCTTY,
and dup2s it onto stdio; falls back to /dev/null + /init.log
- mount_config_iso() / unmount_config_iso(): mounts the KRUN_CONFIG
ISO 9660 image at /mnt via nmount(2) so the JSON config file can be
read, then unmounts it afterwards
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: wire main.rs with full boot sequence
Connect all modules in main() in order:
1. mount_block_root() [amd-sev | tdx]
2. mount_filesystems()
3. mount_block_root_device() [KRUN_BLOCK_ROOT_DEVICE]
4. mount_shared_root()
5. setsid + TIOCSCTTY
6. setup_network()
7. config::load()
8. mount_tmpfs() [config tmpfs mount]
9. apply_env / apply_hostname / apply_rlimits
10. chdir to workdir
11. run_workload(argv)
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement I/O redirects and workload launch
Add exec.rs with:
- setup_redirects(): walks /sys/class/virtio-ports and dup2s
krun-stdin/stdout/stderr onto the corresponding file descriptors
- set_exit_code(): reports the workload exit code to the host via
KRUN_EXIT_CODE_IOCTL, only when the root fs is virtiofs
- run_workload(): forks so PID 1 can reap children; the child calls
exec_workload() which sets up redirects and execvp's the argv.
Parent waits for the child, reports exit code, syncs, and reboots.
KRUN_INIT_PID1=1 skips the fork and exec_workload directly as PID 1.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement environment and resource setup
Extend env.rs with:
- apply_hostname(): sets hostname from HOSTNAME env var, defaulting
to "localhost"
- apply_env(): maps KRUN_HOME -> HOME and KRUN_TERM -> TERM
- apply_rlimits(): parses the KRUN_RLIMITS comma-separated list of
id,cur,max triples and applies each via setrlimit(2)
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement network interface setup
Add setup_network() and setup_dhcp() to env.rs.
setup_network() brings up lo unconditionally. setup_dhcp() checks that
the interface exists before calling do_dhcp(), and logs a warning on
failure rather than aborting (DHCP failure is non-fatal — the VM may be
IPv6-only or have no network).
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement JSON config parsing
Add init/src/config.rs, replacing the hand-rolled jsmn-based parser
with serde_json. Parses /.krun_config.json (or KRUN_CONFIG env var) and
returns a Config struct with:
- argv: Entrypoint ++ (args | Cmd), or None if absent
- workdir: WorkingDir or Cwd
- tmpfs: first tmpfs mount destination not already mounted
Environment variables from the Env array are applied during parsing,
with HOME and TERM always overwritten, all others set only if unset.
A missing or unparseable config file is silently ignored.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement DHCP client
Port init/dhcp.c to Rust in init/src/dhcp.rs. The public surface is a
single do_dhcp(iface) function with the same behaviour as the C version:
- Sends DHCPDISCOVER with Rapid Commit (option 80)
- On DHCPACK: applies address, route, MTU, and DNS directly
- On DHCPOFFER: completes the 4-way handshake, then applies
- On no response: returns Ok (VM may be IPv6-only)
Netlink structs not exposed by libc (ifinfomsg, ifaddrmsg, rtmsg) are
defined locally with #[repr(C)]. sockaddr_nl and sockaddr_in are
zero-initialised via mem::zeroed() to handle opaque padding fields.
Assisted-by: Claude Code:claude-sonnet-4.6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init: implement KRUN_BLOCK_ROOT_DEVICE pivot and shared root mount
Extend fs.rs with:
- try_mount(): mounts with a known fstype, or probes /proc/filesystems
when fstype is None
- mount_block_root_device(): handles KRUN_BLOCK_ROOT_DEVICE by mounting
the block device at /newroot, issuing KRUN_REMOVE_ROOT_DIR_IOCTL to
drop the virtiofs temporary root, then pivoting with MS_MOVE
- mount_shared_root(): sets MS_REC|MS_SHARED propagation on /
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: implement filesystem mounting
Add init/src/fs.rs with:
- mount_or_busy(): helper that treats EBUSY as success
- mount_filesystems(): mounts devtmpfs, proc, sysfs, cgroup2,
devpts, tmpfs(/dev/shm), and creates the /dev/fd symlink
- is_mount_point(): parses /proc/mounts (avoids triggering Podman
auto-mounts that stat() would cause)
- mount_tmpfs(): mounts a tmpfs at an arbitrary path
Implement mount_tee_block_root() function used
by both SEV and TDX features to mount /dev/vda and chroot into it.
For amd-sev this replaces the previous LUKS/KBS attestation path
entirely. The SEV and TDX boot paths are now identical at the init level.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
init: scaffold Rust krun-init crate and wire into build system
Replace the C-based build_default_init() in src/devices/build.rs with a
Rust crate (init/) compiled via a cargo subprocess. The new build.rs
probes whether the active rustc supports the $(uname -m)-unknown-linux-musl
target (for a static binary) and falls back to the native target with a
user-visible warning if not.
The KRUN_INIT_BINARY_PATH override mechanism is preserved so that
out-of-tree binaries (e.g. pre-built SEV or TDX images) can still be
injected without rebuilding.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
Assisted-by: Claude Code:claude-sonnet-4.6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
ci: trust libkrun/krun tap before installing formulae on macOS
Homebrew now refuses to install formulae from third-party taps
that haven't been explicitly trusted, breaking all macOS CI jobs.
Assisted-by: Claude Code:claude-opus-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
AGENTS.md: fix agent attribution format
The agent attribution in each commit is missing the "Assisted-by"
prefix.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
virtio/balloon: add Windows support for free-page reporting
Use DiscardVirtualMemory as the Windows equivalent of madvise for releasing guest memory in the balloon free-page reporting queue. Gate the unix AsRawFd import and add the Windows shim for the event handler.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/console: split port_io into platform-specific modules
Replace the single port_io.rs file with a port_io/ directory containing mod.rs (shared traits and types), unix.rs, and windows.rs. The public API is unchanged; all platform-specific implementations are now isolated in their respective modules instead of being interleaved with #[cfg] annotations throughout a single file.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/console: add Windows support
Add Windows backends for console I/O using native HANDLEs instead of
Unix file descriptors. Introduce SendHandle in utils for thread-safe
handle passing, platform-specific PortInput/PortOutput/TerminalProperties
implementations using ReadFile/WriteFile/WaitForMultipleObjects, Windows
terminal raw mode via Get/SetConsoleMode, and the corresponding C API
surface with void* handle parameters behind #ifdef _WIN32.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
deps: cfg-gate unix-only crates and fix vm-memory for Windows
Move nix to [target.'cfg(unix)'.dependencies] in devices and vmm.
Disable vm-memory default features (the rawfd feature is unsupported
on Windows) across kernel, smbios, devices, and vmm. Add windows-sys
to devices, vmm, and libkrun for Windows console APIs. Bump imago
to 0.2.3.
Signed-off-by: lstocchi <lstocchi@redhat.com>
libkrun: expose init-blob in krun_has_feature()
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
Makefile: support INIT_BLOB=0 to disable bundled init binary
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
libkrun: add "init-blob" cargo feature to gate bundled init binary
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init_blob: align crate with the workspace
Rename directory from init-blob to init_blob, set edition to 2024,
rename the package to krun-init-blob, and set version to 0.1.0-1.18.0.
Signed-off-by: Sergio Lopez <slp@redhat.com>
fs/macos: on readdir, refresh directory on rewind
Use offset == 0 as an indicator that we're dealing with either a fresh
directory stream or one that has been rewound. If that's the case,
ensure the cache will be refreshed.
Suggested-by: Jay Gowdy <jgowdy@godaddy.com>
Signed-off-by: Sergio Lopez <slp@redhat.com>
tests/virtiofs-misc: add rename-overwrite fchmod subtest
Add regression test for PR #700, which fixed a file descriptor leak
on macOS when renaming over an existing file with open fds.
The bug: macOS virtio-fs uses volfs paths (/.vol/{dev}/{ino}) which
become invalid when the inode's last link is removed. When rename()
replaces a file, the old target loses its directory entry, breaking
subsequent operations on still-open file descriptors.
This broke apt/dpkg atomic writes with errors like:
"Problem closing the file /var/lib/dpkg/status - close (2: No such file or directory)"
The fix stores an fd to the doomed target before rename, preserving
access to the unlinked inode for tracked fds.
The test verifies that fchmod() works on an open fd after the file
has been atomically replaced via rename.
Assisted-by: Claude Code:claude-opus-4.6
Signed-off-by: Sergio Lopez <slp@redhat.com>
fs/macos: preserve the inode a rename overwrites
The macOS virtio-fs passthrough addresses inodes by their volfs path
("/.vol/{dev}/{ino}"), which only resolves while the inode still has a
directory entry. A rename that REPLACES an existing target drops that
target's last link, but unlike do_unlink -- which stashes an fd to the
doomed inode in InodeData.unlinked_fd -- rename never preserved the
overwritten inode. Any FUSE op the guest later issues on it then resolves
a dangling volfs path and returns ENOENT.
This breaks the common atomic write-new-then-rename-over pattern when the
guest still holds the old target open: apt/dpkg rewrite /var/lib/dpkg/status
this way while apt holds it open, so apt's final close reports
"Problem closing the file /var/lib/dpkg/status - close (2: No such file or
directory)" even though the install succeeded. It reproduces on any
overlayfs upperdir backed by this server.
Mirror do_unlink: grab an fd to the target before renamex_np (replace case
only -- not RENAME_SWAP/RENAME_EXCL) and hand it to the displaced inode on
success. store_unlinked_fd now reports whether a tracked inode took
ownership so the caller closes the fd otherwise instead of leaking it.
Signed-off-by: Alex NOVAC <acdtrx@gmail.com>
init: loosen perms on /dev/kvm if it exists
Best effort loosen /dev/kvm permissions to allow nested virtualization by
unprivileged processes inside the microVM (usually a single purpose
environment). Log errors but don't log ENOENT since the guest kernel may
not support KVM or nested virtualization might not be enabled.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Dusty Mabe <dusty@dustymabe.com>
devices: add WHP IOAPIC backend for Windows x86_64
WHP emulates the LAPIC but not the IOAPIC. Add a WhpIoapicBackend that
plugs into the common IOAPIC register emulation and delivers interrupts
through WHvRequestInterrupt.
The backend translates IOAPIC redirection-table entries into WHP
InterruptRequest structs, handling physical/logical destination modes,
edge/level trigger modes, and delivery modes (Fixed, LowestPriority, NMI,
INIT, SIPI). Level-triggered interrupts track Remote-IRR as the KVM
backend does.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
devices: refactor IOAPIC to support multiple hypervisor backends
This commit refactors the IOAPIC emulation logic to decouple common register state management from hypervisor-specific interrupt injection. This is a prerequisite for supporting the Windows Hypervisor Platform (WHP).
It introduces the IoApicBackend trait to handle backend-specific logic such as interrupt routing updates and injection.
It moves the register state into a dedicated IoApicRegs struct, making the core IoApic struct generic over the backend (IoApic<B: IoApicBackend>) and place KVM-sspecific implementation into a new ioapic_kvm.rs file.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/net: fix ENAMETOOLONG on long gvproxy socket paths
macOS imposes a 104-byte limit on unix socket paths. The gvproxy peer
socket path can be long enough that appending a suffix to derive the
local bind address exceeds this limit, triggering ENAMETOOLONG. This
surfaced as BadActivate from the net device, panicking the vCPU thread
and leaving the VM frozen at 0% CPU while krunkit stayed alive.
Place the local bind socket in the OS default temporary directory using a
short PID + per-process atomic counter filename (krun-net-<pid>-<n>.sock).
PID + counter guarantees uniqueness across concurrent krunkit processes
and multiple virtio-net devices within a single process.
Add a net-gvproxy-long-path integration test that constructs a peer
socket path long enough that the old suffix-based approach would exceed
the 104-byte limit, and verifies that the VM starts and achieves network
connectivity through gvproxy.
Assisted-by: Claude Code (Sonnet 4.6)
Signed-off-by: arewm <arewm@users.noreply.github.com>
whp: add Hyper-V enlightenment support and invariant TSC
Configure the WHP partition to expose Hyper-V enlightenments to the
guest, enabling it to use synthetic MSRs for timekeeping and
hypercalls instead of expensive VM exits on native hardware interfaces.
- Advertise Hyper-V CPUID leaves 0x40000000–0x40000006 (signature,
interface ID, version, feature identification, recommendations,
implementation limits, and hardware features).
- Enable synthetic processor features: HypervisorPresent, Hv1,
reference TSC, reference counter, frequency registers, hypercalls,
and VP index/runtime access.
- Set the MSR exit bitmap so unhandled MSR accesses trigger a VM exit
rather than injecting #GP into the guest.
- Retrieve and re-apply processor features banks with the invariant
TSC bit set, and expose it via CPUID 0x80000007.
- Register CPUID leaf 1 in the exit list so the hypervisor-present
bit (ECX.31) can be injected, which is required for the guest to
query the 0x40000000+ range.
Assisted-by: Cursor:claude-opus-4.6
Signed-off-by: lstocchi <lstocchi@redhat.com>
tests/virtiofs-misc: add mid-iteration duplicate detection subtest
Add a test that creates files while iterating a directory via readdir
and asserts no duplicate entries are returned. The test inserts entries
likely to shift iteration order, maximizing the chance of exposing
duplicates.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests/virtiofs-misc: add DirStream cache coherence subtests
Add 7 subtests that exercise readdir visibility after directory
mutations on an already-open handle: create, unlink, mkdir/rmdir,
symlink, link, rename (same-dir), and rename (cross-dir). Each test
opens a directory, reads entries to populate the cache, mutates the
directory, calls rewinddir, and asserts the mutation is visible.
These reproduce the macOS virtiofs DirStream cache coherence bug
fixed in PR #693.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests/virtiofs-misc: run subtests with catch_unwind
Wrap each subtest in catch_unwind so that a failure in one subtest
does not prevent the remaining subtests from running. The output now
lists each subtest as PASS/FAIL individually.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
balloon: use MADV_FREE to reclaim memory on macOS
The MADV_DONTNEED flag is no-op on macOS because it preserves the data,
so the OS cannot discard the pages. MADV_FREE flag actually frees
the memory under pressure, which is what we need for balloon.
Signed-off-by: Vadzim Dambrouski <pftbest@gmail.com>
init/dhcp: only overwrite resolv.conf with DNS
Only overwrite resolv.conf when the DHCP server has provide us some
nameservers. This allows users to keep the original resolv.conf in case
it was populated beforehand.
Signed-off-by: Sergio Lopez <slp@redhat.com>
arch: implement x86_64 Windows/WHP backend
Add the WHP-specific register, segment, and MSR configuration in
src/arch/src/x86_64/windows/. Uses structured WHV_REGISTER_VALUE writes
for FP registers and proper WHP register name mapping for MSRs. Includes
the get_page_size helper for Windows.
Signed-off-by: lstocchi <lstocchi@redhat.com>
arch/x86_64: infrastructure for Windows/WHP
Introduce src/arch/src/x86_64/linux/ and windows/ folder structures.
Move existing Linux/KVM code to linux/ and add platform re-exports
with cfg(target_os) conditional compilation.
Signed-off-by: Sergio Lopez <slp@redhat.com>
Signed-off-by: lstocchi <lstocchi@redhat.com>
arch/x86_64: decouple register computation
Move the logic that calculates GDT/IDT and page tables into
compute_segments() and compute_page_tables() functions that return
hypervisor-agnostic BootSegments and BootPageTables structs. Existing
KVM setup functions now delegate to these.
Signed-off-by: lstocchi <lstocchi@redhat.com>
arch/x86_64: abstract segment handling
Introduce a hypervisor-agnostic SegmentDescriptor struct and rename
kvm_segment_from_gdt to segment_from_gdt. The KVM backend uses a
kvm_segment_from conversion helper, proving the agnostic struct works
before adding the Windows path.
Signed-off-by: lstocchi <lstocchi@redhat.com>
virtio/fs: report actual bytes written in used ring len
The virtio used ring's len field must reflect the number of bytes the
device wrote into the writable descriptor chain. libkrun was hard-coding
0, which broke FUSE protocol validation added in Linux kernel commit
68b69fa0edb (virtiofs: add FUSE protocol validation).
Resolves: https://github.com/containers/libkrun/issues/695
Signed-off-by: Gris Ge <cnfourt@gmail.com>
style: apply cargo fmt after 2024 edition migration
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
tests: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
krun-sys: migrate to Rust 2024 edition, bump rust-version to 1.85
Add [workspace] to mark it as a standalone crate (not a workspace
member), bump edition to 2024 and rust-version to 1.85.0. No source
changes were required by cargo fix.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
libkrun: migrate to Rust 2024 edition
- Bump edition from 2021 to 2024 in Cargo.toml
- Wrap #[no_mangle] with #[unsafe(no_mangle)] (84 occurrences)
- Add unsafe {} blocks inside unsafe fn bodies (unsafe-op-in-unsafe-fn)
- Collapse nested if-let blocks flagged by clippy::collapsible_if
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
aws_nitro,whp: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
vmm: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
rutabaga_gfx: fix virgl_renderer unsafe issues missed in 2024 migration
The virgl_renderer feature requires system libraries not available during
the initial migration, so these code paths were not reached by cargo fix.
Also fix two collapsible_if clippy warnings in virtio/gpu that only
surface when compiling krun-devices with the gpu feature.
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
devices: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
rutabaga_gfx: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
hvf: migrate to Rust 2024 edition
- Add `unsafe` to all 79 extern "C" blocks (78 in bindings.rs, 1 in lib.rs)
- Remove redundant `ref` binding modifier in match arm (2024 edition error)
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
kernel: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
arch: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
cpuid: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
arch_gen,utils,smbios,polly: migrate to Rust 2024 edition
Assisted-by: Claude Code: claude-sonnet-4-6
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
tests: consistently use `get_krun_add_net_unixgram`, don't duplicate the definition
Signed-off-by: Jan Noha <nohajc@gmail.com>
update `integration_tests.yml` to run FreeBSD tests
- have to install lld and also clang on the self-hosted runner
Signed-off-by: Jan Noha <nohajc@gmail.com>
tests: add FreeBSD guest support
- simple boot test, gvproxy connect and listen tests
- fix timing issue in all network listen tests
- replace the macOS-only `cfg` gate with a `gvproxy -version` check
Signed-off-by: Jan Noha <nohajc@gmail.com>
arch/vmm: add PVH boot protocol support for FreeBSD x86_64
Implement PVH (Para-Virtualized Hardware) boot protocol support,
enabling FreeBSD x86_64 kernels to boot via the Xen PVH ABI.
Key changes:
- arch/Cargo.toml: add linux-loader (elf feature) as x86_64 dep
- layout.rs: add PVH_INFO_START, MODLIST_START, MEMMAP_START, RSDP_ADDR
- mptable.rs: export MPTABLE_START as pub
- gdt.rs: fix get_limit() to apply granularity (G) bit expansion
- mod.rs: add configure_pvh(), add_memmap_entry(); refactor
configure_system() to dispatch to configure_pvh() or
configure_64bit_boot() based on pvh flag; add pvh param
- regs.rs: add pvh param to setup_regs/setup_sregs/
configure_segments_and_sregs; set 32-bit protected mode GDT and
CR0=PE|ET for PVH; set rbx=PVH_INFO_START per PVH ABI
- vstate.rs: thread pvh through configure_x86_64()
- builder.rs: detect PvhEntryPresent in ELF load result; propagate
pvh through PayloadConfig, load_payload, create_vcpus_x86_64
- lib.rs: add pvh param to Vmm::configure_system()
Co-authored-by: Dmitrii Sharshakov <d3dx12.xx@gmail.com>
Assisted-by: Copilot:claude-opus-4.6
Signed-off-by: Jan Noha <nohajc@gmail.com>
CI: Check for improper AI atribution
Add a script to enforce rules mentioned in CONTRIBUTING.md.
Main motivation for this is banning Co-Authored-By: Claude.
This also checks for other ill-formated Assisted-By forms and generates a
warning.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
CONTRIBUTING.md: Update Assisted-by/Generated-by guidlines
We previously only allowed using:
Assisted-by: Claude Code:claude-opus-4.6
Update the guidlines to also other well formated alternatives:
Assisted-by: Claude Code / claude-opus-4.6
Assisted-by: Copilot (GPT-5.4)
Also allows a similar Generated-By tag:
Generated-by: OpenCode:claude-opus-4.6
This also explicitly codifies you can use `auto` or `unknown` in case you don't
know the actual model name. This can be used when you are using automatic
selection in tools like Cursor or Github Copilot.
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init: replace dummy IP address
In commit 2593accd9b, we extended init to configure a dummy interface
when TSI is enabled, to help local applications find out the VM has
network connectivity.
For the dummy interface, we've used the 10.0.0.1/8 address, which
collides with the network ranges used by podman by default. In
principle, this shouldn't be a problem since, with TSI hijack enabled,
the kernel takes over the application's INET socket.
But, to implement both the INET and VSOCK personalities, when connect()
is called on a TSI socket, TSI attempts to connect to the endpoint first
with the INET personality, falling back to VSOCK only if that one fails.
For SOCK_DGRAM sockets, a connect() via the INET personality will always
succeed if there's a route defined that would allow to reach the
destination address.
So, with a dummy inteface configured with a 10.0.0.1/8 address,
attempting to access a DNS server within that range leads to the
following flow:
- Userspace creates the AF_INET/SOCK_DGRAM socket.
- The kernel hijacks the AF_INET returning an AF_TSI socket instead.
- Userspace calls connect() on the socket.
- tsi_connect() attemps connecting via the INET personality first, with
success.
- For TSI, the socket becomes effectively an AF_INET socket, not
allowing external communitations via VSOCK.
To avoid this issue, let's switch to an address from IANA's TEST-NET-3
block, which shouldn't be used by any network external to the VM.
Signed-off-by: Sergio Lopez <slp@redhat.com>
virtio/fs: inline do_lookup() into lookup()
do_lookup() originally existed to hold the init.krun special-case
logic separate from the trait method. Now that AugmentFs handles
init.krun, do_lookup() has no special cases and can be inlined.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
CI: enable BLK=1 and install e2fsprogs in integration tests
Build and test with the block device feature so the root-disk-remount
test runs in CI. Install e2fsprogs (provides mke2fs) which the test
needs to create the ext4 disk image.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests: add root-disk-remount integration test
Boot from an ext4 block device via krun_set_root_disk_remount. The
virtiofs root uses NullFs with init.krun and virtual mount-point
directories overlaid. The guest verifies it pivoted to the block
device root successfully.
Uses dlsym for krun_add_disk/krun_set_root_disk_remount so the test
compiles without BLK and skips gracefully at runtime.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
tests: add augmentfs integration test
Boot a VM with a pure NullFs root — no host directory at all. Every
file in the root (init.krun, guest-agent, .krun_config.json, test
data) is injected as a virtual overlay, and /dev, /proc, /sys are
virtual empty directories used as mount points.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: move EXIT_CODE ioctl to AugmentFs
The exit-code ioctl is a krun mechanism, not a filesystem operation.
Move it to the AugmentFs overlay where it is handled before any
delegation to the inner filesystem.
The Linux passthrough retains only EXPORT_FD (which needs access to
passthrough-internal handle and export tables). The macOS passthrough
no longer implements ioctl at all.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: remove REMOVE_ROOT_DIR ioctl and allow_root_dir_delete
The temporary root directory hack is gone (replaced by NullFs), so
the ioctl that cleaned it up and the config flag that gated it are
no longer needed. Remove allow_root_dir_delete from FsDeviceConfig,
Fs::new(), passthrough Config, and all call sites.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: rewrite krun_set_root_disk_remount to use NullFs
krun_set_root_disk_remount no longer creates a temporary empty host
directory. Instead it configures a NullFs-backed virtiofs device
(shared_dir: None) with init.krun overlaid via AugmentFs.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: add NullFs, a minimal empty-root FileSystem
NullFs implements the FileSystem trait with just an empty root
directory. It can be wrapped with AugmentFs to serve virtual
files without any host directory involvement.
Fs::new() now accepts Option<String> for shared_dir — None selects
NullFs. FsDeviceConfig and FsServer gain the corresponding variants.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
libkrun.h: document that implicit resource creation will become opt-in
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: add krun_get_default_init()
Add API to retrieve the built-in default init binary. Callers that
use krun_disable_implicit_init() can use this to obtain the init
binary and inject it themselves via krun_fs_add_overlay_file().
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: add krun_fs_add_overlay_file(), krun_fs_add_overlay_dir() APIs
Add C APIs to inject virtual files and directories into a virtiofs
device. Entries are backed entirely by host memory (no host file).
Files support one-shot semantics (disappear after the first lookup).
Paths may contain '/' to nest entries inside existing virtual
directories (e.g. krun_fs_add_overlay_dir for "etc", then
krun_fs_add_overlay_file for "etc/hostname"). Intermediate
directories must already exist; -ENOENT / -ENOTDIR is returned
otherwise.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
lib: add krun_disable_implicit_init()
Add API to prevent the default init binary (/init.krun) from being
injected into the root filesystem. Follows the existing
krun_disable_implicit_{console,vsock} pattern.
Must be called before krun_set_root().
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: introduce generic AugmentFs overlay for files like init.krun
Introduce AugmentFs<T>, a generic overlay that wraps any FileSystem
implementation and intercepts FUSE operations for virtual inodes —
synthetic read-only files and directories backed by static data.
One-shot files can only be looked up once.
Remove all init.krun special-case code (init_inode, init_handle,
INIT_CSTR) from both the Linux and macOS passthrough implementations.
The init.krun virtual file is now configured via VirtualDirEntry in
the krun API layer and handled generically by the overlay.
FsDeviceConfig carries a Vec<VirtualDirEntry> and FsWorker wraps
AugmentFs<PassthroughFs> / AugmentFs<PassthroughFsRo>.
The InodeAllocator now starts at ROOT_ID + 1 since the init_inode
reservation is no longer needed.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: introduce InodeAllocator for shared inode numbering
Replace the private next_inode AtomicU64 inside PassthroughFs with a
shared InodeAllocator that is passed in at construction. This lets
multiple layers (e.g. a future virtual-inode overlay) allocate from
the same counter without implicit coordination via reserved ranges.
The allocator starts at ROOT_ID + 2, reserving inode 2 for the
existing init_inode in PassthroughFs. This reservation is removed
in the next commit when init handling moves to AugmentFs.
PassthroughFs::new() and PassthroughFsRo::new() now take an
Arc<InodeAllocator> parameter. FsWorker::new() creates the allocator
and passes it through.
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
virtio/fs: extract init binary blob into its own crate
Move the init binary build script and include_bytes!() from the
devices crate into a new init-blob crate. The passthrough modules
reference the binary as init_blob::INIT_BINARY instead of using
include_bytes! directly.
Inspired by https://github.com/containers/libkrun/pull/593.
Suggested-by: Geoffrey Goodman <geoff@goodman.dev>
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
Add AGENTS.md and .claude
Adds an AGENTS.md file to give agents context about libkrun. This
includes project structure and coding standards.
Create a .claude/ directory that points the Claude Code agent to the AGENTS.md file since
that file isn't supported.
This was initially generated by running /init inside of Claude Code. I
took some sections from
https://github.com/multica-ai/andrej-karpathy-skills/blob/main/CLAUDE.md?plain=1
and
https://github.com/openai/openai-agents-python/blob/f20aa40f56ec374841dfe6e4f3d6118c8e32fb2b/AGENTS.md.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
init/aws-nitro: Continue if module already loaded
If a module is already loaded, then the initramfs can continue on
loading other modules. Although returning an error, this does not
require failure of the initramfs.
Signed-off-by: Tyler Fanelli <tfanelli@redhat.com>
devices/vsock: support multi-descriptor TX chains
from_tx_virtq_head assumed a fixed 2-descriptor layout (header + one
data descriptor), which breaks with newer kernels that may combine
header and data in a single descriptor (Linux 6.2+) or split data
across multiple descriptors.
Handle three cases: single combined descriptor (zero-copy), classic
two-descriptor (zero-copy, unchanged), and multi-descriptor data
(copied into an owned contiguous buffer). The RX path already handled
the combined case; this brings the TX path to parity and beyond.
Assisted-by: Claude Code:claude-opus-4.6
Signed-off-by: Sergio Lopez <slp@redhat.com>
init: fix unescape_string() not advancing past escape characters
The unescape_string() function in init.c, which handles JSON escape
sequences when parsing environment variables from .krun_config.json,
had a bug where the pointer 'val' was not advanced past the escape
character after processing it.
When encountering a two-character JSON escape sequence like \n or \",
the switch statement pre-increments val to point at the escape
character (e.g., 'n' or '"') and writes the unescaped byte to the
output. However, it never advances val past that character. On the
next loop iteration, the character is not a backslash, so it gets
copied again as a literal character.
This causes:
- \n (JSON-escaped newline) to produce a newline followed by a
literal 'n'
- \" (JSON-escaped double quote) to produce two double quotes
For example, an environment variable set to a JSON string like:
{\"key\": \"value\"}
would be rendered inside the krun VM as:
{""key"": ""value""}
Fix this by adding val++ after writing the unescaped character in
each case of the switch statement. The 'u' (unicode) case already
handles its own pointer arithmetic and is not affected.
Fixes: https://github.com/containers/libkrun/issues/678
Assisted-by: <anthropic/claude-opus-4.6>
Signed-off-by: Dusty Mabe <dusty@dustymabe.com>
README: add security email section
Add a section telling users to report security issues to
libkrun-security@redhat.com instead of the GH issue tracker or Matrix.
Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
README.md: link to CONTRIBUTING.md
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
Add contribution guidelines
Codify existing conventions for commits, DCO sign-off, rebase merging,
and add a policy for AI-assisted contributions (inspired by the Linux
kernel's).
Assisted-by: OpenCode:claude-opus-4.6
Signed-off-by: Matej Hrica <mhrica@redhat.com>
rutabaga_gfx/cross_domain: fix audio glitches by dropping write lock
The previous write() held item_state.lock() across the write_volatile call
that performs the actual syscall, serializing every cross-domain CMD_WRITE
behind any other operation on the items table — including add_item from
process_receive, which fires for every new fd received via SCM_RIGHTS as
guests open additional channels.
For per-period eventfd wakeups (e.g. PipeWire audio streams signaling host
playback every audio period), this means the write completes only after any
in-flight item_state operation finishes. Under stream-create churn — many
guest applications opening streams concurrently, each delivering new fds via
SCM_RIGHTS that hit add_item under the same lock — the wait can exceed the
audio period budget and produce missed-deadline glitches at the host's
audio output.
This change shrinks the critical section to a brief fd dup() under the lock,
performs the syscall lock-free, and only re-acquires the lock if hang_up
indicates the item should be removed. In the common case (hang_up == 0,
e.g. repeated eventfd wakeups for an active stream) the table is no longer
touched per write, eliminating both the contention and the previous
"remove + conditional re-insert" churn.
Behavioral changes vs the old code:
- Common case (hang_up == 0): item stays in the table; we hand out a dup'd
fd for the write. Net behavior identical, lock hold time bounded by dup().
- hang_up == 1: item removed in a separate phase after the write, instead
of "removed unconditionally then re-inserted on hang_up == 0". Same
observed end state.
- Concurrent writes to the same id (no longer serialized): the kernel
guarantees atomicity for eventfd writes (8 bytes) and pipe writes
<= PIPE_BUF, which are the only two CrossDomainItem variants this branch
handles. Each caller dup's its own fd and writes through it independently.
Verified with a synthetic reproducer: a sustained 1 kHz sine playing through
a guest PipeWire stream while ~200 short-lived guest streams open and close
concurrently (each issuing SCM_RIGHTS for new eventfds, contending with the
sustained stream's per-period writes for item_state). Capturing the host
sink monitor and counting sample-to-sample deltas exceeding a clean-sine
threshold, the stress workload produced ~10 distinct glitch bursts in an
8-second capture before this change, and zero across five consecutive runs
after.
Signed-off-by: Adam Ford <adam.ford@anodize.com>
whp: implement safe wrappers for VM, vCPU, and instruction emulation
This commit introduces the foundational Rust abstractions for the Windows
Hypervisor Platform (WHP) backend. It encapsulates the raw C FFI bindings
into safe, idiomatic Rust structs (`WhpVm`, `WhpVcpu`, and `WhpEmulator`).
Key features include:
- Partition & vCPU Management: Safe wrappers for partition configuration,
CPUID masking, memory mapping, and `WHvRunVirtualProcessor` exit routing.
- High-Performance Register I/O: Introduces const-generic helpers
(`get_registers<N>` and `set_registers<N>`) to guarantee stack-allocated,
zero-overhead register reads/writes on the VM-exit hot path.
- Robust TSC Calibration: Implements accurate host TSC frequency detection
via CPUID 0x15/0x16, with a reliable `QueryPerformanceCounter` (QPC)
calibration fallback for AMD/older hardware.
- Emulation: Integrates WHP's built-in x86 emulator for MMIO and PIO handling.
Signed-off-by: lstocchi <lstocchi@redhat.com>
Remove builtin virtio-snd device
The builtin virtio-snd device is not actively maintained or tested and
has been superseded by vhost-user-snd. Remove it as part of the libkrun
2.0 breaking changes.
This removes:
- The virtio-snd device implementation (src/devices/src/virtio/snd/)
- The 'snd' feature flag from all crates
- The pipewire (pw) dependency from devices
- The krun_set_snd_device() C API and KRUN_FEATURE_SND constant
- CI references to the snd feature and libpipewire-0.3-dev
Signed-off-by: Matej Hrica <mhrica@redhat.com>
init: Set up a dummy network interface with TSI
Some applications check for network availability by looking for a
network device configured for Internet access. When TSI is used, there
is no such device available by default, although Internet is accessible.
Then those applications behave like when the connection is not
available.
Let's solve this problem by setting up a dummy network interface. The
dummy interface is automatically created when CONFIG_DUMMY is enabled in
kernel or the corresponding kernel module is loaded. This means a
sufficiently recent libkrunfw version is needed (see
https://github.com/containers/libkrunfw/pull/116). The dummy interface
is initially down.
In order to make the applications happy, the interface must be brought
up and set up for Internet connections. This is ensured by setting the
IP address to 10.0.0.1/8 (an arbitrary choice without any special
reason) in init.c if TSI is enabled. The netmask is selected to be
sane; it doesn't cover the whole IP range and we cannot set a default
route because then TSI has problems, but it's OK for the tested
application. We can change it if some application has trouble with
that.
TSI availability is determined by checking the presence of `tsi_hijack'
in the kernel command line, before `--' delimiter if present.
The dummy interface simply swallows all packets. But it is effectively
bypassed by TSI for practical purposes. Things like ICMP don't work in
either case.
When the kernel support is not available, the device is not present and
init.c cannot set it up. We skip the configuration silently in such a
case, to not spam users with errors if they use older libkrunfw or
custom kernels.
Fixes: #576
Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
rutabaga_gfx/cross_domain: handle CMD_WRITE on Eventfd items
The cross_domain `write` handler only matched
`CrossDomainItem::WaylandWritePipe`, falling into the catch-all for
every other item type after the unconditional `remove()` at the top of
the function had already dropped the entry from the table. PipeWire
(and other clients that share host-created eventfds via SCM_RIGHTS for
per-period wakeups) sends CMD_WRITE on those eventfd identifiers — the
first such write returns InvalidCrossDomainItemType *after* removing
the item, and every subsequent write on the same identifier returns
InvalidCrossDomainItemId, masquerading on the guest as the opaque
VIRTIO_GPU_RESP_ERR_UNSPEC (0x1200).
Reproduced inside a libkrun guest on x86_64 by routing PipeWire's
ALSA-shim audio through a host PipeWire daemon. With a paired BT
speaker as the sink, `speaker-test -D pipewire -c2 -t wav -l 1`
produces, per stream, ~10 entries of:
[ 0.682819] [drm:virtio_gpu_dequeue_ctrl_func] *ERROR* response 0x1200 (command 0x207…
The macOS virtio-fs passthrough addresses inodes by their volfs path ("/.vol/{dev}/{ino}"), which only resolves while the inode still has a directory entry. A rename that REPLACES an existing target drops that target's last link, but unlike do_unlink -- which stashes an fd to the doomed inode in InodeData.unlinked_fd -- rename never preserved the overwritten inode. Any FUSE op the guest later issues on it then resolves a dangling volfs path and returns ENOENT.
This breaks the common atomic write-new-then-rename-over pattern when the guest still holds the old target open: apt/dpkg rewrite /var/lib/dpkg/status this way while apt holds it open, so apt's final close reports "Problem closing the file /var/lib/dpkg/status - close (2: No such file or directory)" even though the install succeeded. It reproduces on any overlayfs upperdir backed by this server.
Mirror do_unlink: grab an fd to the target before renamex_np (replace case only -- not RENAME_SWAP/RENAME_EXCL) and hand it to the displaced inode on success. store_unlinked_fd now reports whether a tracked inode took ownership so the caller closes the fd otherwise instead of leaking it.
Built on macOS/aarch64 and verified in a downstream consumer — apt-based setup recipes that previously failed at dpkg's close now succeed.