From 2a5d3af615b298c747cdb11cfefdc6cb08cd332d Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Sun, 16 Aug 2026 20:56:13 +0200 Subject: [PATCH 1/5] Give the guest kernel identity one definition uname(2) and /proc/version each spelled the reported kernel as a private string literal, in syscall/sys.c and runtime/procemu.c, free to drift apart. Both now read GUEST_KERNEL_RELEASE and GUEST_KERNEL_VERSION from syscall/sys.h, which procemu.c already includes. The reported values are unchanged. Verified: the new tests/test-proc.c case pins the uname release into the /proc/version banner, and fails when the two are forced apart. --- src/runtime/procemu.c | 8 ++++---- src/syscall/sys.c | 4 ++-- src/syscall/sys.h | 6 ++++++ tests/test-proc.c | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index d57bec36..edec1071 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -2736,11 +2736,11 @@ int proc_intercept_open(const guest_t *g, /* /proc/version -> synthetic kernel version string */ if (!strcmp(path, "/proc/version")) { return proc_emit_literal( - "Linux version 6.17.0-20-generic " - "(buildd@bos03-arm64-051) " + "Linux version " GUEST_KERNEL_RELEASE + " (buildd@bos03-arm64-051) " "(aarch64-linux-gnu-gcc (Ubuntu 15.2.0-4ubuntu4) " - "15.2.0, GNU ld (GNU Binutils for Ubuntu) 2.45) " - "#20-Ubuntu SMP PREEMPT_DYNAMIC\n"); + "15.2.0, GNU ld (GNU Binutils for Ubuntu) " + "2.45) " GUEST_KERNEL_VERSION "\n"); } /* /proc/filesystems, /proc/self/mountinfo, /proc/mounts, /etc/mtab. */ diff --git a/src/syscall/sys.c b/src/syscall/sys.c index 72b515d5..2310dda0 100644 --- a/src/syscall/sys.c +++ b/src/syscall/sys.c @@ -44,8 +44,8 @@ static const linux_utsname_t cached_uname = { /* Kernel version: match the lima aarch64 VM kernel to avoid version-gated * feature detection mismatches in userspace. */ - .release = "6.17.0-20-generic", - .version = "#20-Ubuntu SMP PREEMPT_DYNAMIC", + .release = GUEST_KERNEL_RELEASE, + .version = GUEST_KERNEL_VERSION, .machine = "aarch64", .domainname = "(none)", }; diff --git a/src/syscall/sys.h b/src/syscall/sys.h index 7c5b9e6c..2d20ed32 100644 --- a/src/syscall/sys.h +++ b/src/syscall/sys.h @@ -15,6 +15,12 @@ #include #include "core/guest.h" +/* Guest-visible kernel identity, shared by uname(2) and /proc/version so the + * two cannot drift apart. tests/test-proc.c pins them to each other. + */ +#define GUEST_KERNEL_RELEASE "6.17.0-20-generic" +#define GUEST_KERNEL_VERSION "#20-Ubuntu SMP PREEMPT_DYNAMIC" + /* System info syscall handlers. */ int64_t sys_uname(guest_t *g, uint64_t buf_gva); diff --git a/tests/test-proc.c b/tests/test-proc.c index a0d839d6..b7435e90 100644 --- a/tests/test-proc.c +++ b/tests/test-proc.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "test-harness.h" #include "test-util.h" @@ -110,6 +111,20 @@ int main(void) FAIL("read failed"); } + /* /proc/version and uname(2) describe one kernel, so the release the + * banner carries has to be the release uname reports. + */ + TEST("/proc/version matches uname"); + { + char buf[512]; + struct utsname uts; + ssize_t n = read_file_nul("/proc/version", buf, sizeof(buf)); + if (n > 0 && uname(&uts) == 0) + EXPECT_TRUE(strstr(buf, uts.release), "release disagrees"); + else + FAIL("read or uname failed"); + } + /* /proc/filesystems: should contain at least one fs type */ TEST("/proc/filesystems"); { From 60bfc566491aff37690c806e27fc8db8502a90dd Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Sun, 16 Aug 2026 20:58:47 +0200 Subject: [PATCH 2/5] Report Linux 6.18 LTS to the guest The old release claimed a lima aarch64 VM kernel, but no lima configuration or provisioning exists in this repo, and the only kernel the tree boots is the qemu oracle's Alpine linux-virt. The release now names the 6.18 LTS baseline, maintained through December 2027, and tracks no image. Against dispatch.tbl, 6.18 is the smaller gap: 6.17 added file_getattr and file_setattr, both above SC_MAX_SYSCALL_NUM and ENOSYS here, while 6.18 added no syscalls. glibc's dl_discover_osversion rejects only a release below its build-time floor, so raising the number is the safe direction. The /proc/version banner loses the buildd@bos03-arm64-051 builder and the Ubuntu toolchain spellings: that host never built this, and no Ubuntu release ships a 6.18 kernel. The builder and compiler fields are now fixed elfuse strings in the proc_version_show format. Coverage: tests/test-comprehensive.c asserts release >= 6.18 where nodename is elfuse, so the same binary passes under the qemu oracle; the check was seen failing with the floor raised past the release. --- src/runtime/procemu.c | 9 ++++----- src/syscall/sys.c | 4 ---- src/syscall/sys.h | 11 +++++++---- tests/test-comprehensive.c | 9 +++++++++ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index edec1071..8215504d 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -2733,14 +2733,13 @@ int proc_intercept_open(const guest_t *g, if (!strcmp(path, "/proc/sys/kernel/randomize_va_space")) return proc_emit_literal("2\n"); - /* /proc/version -> synthetic kernel version string */ + /* /proc/version -> synthetic banner in the kernel's proc_version_show + * format; the builder and compiler fields are fixed strings. + */ if (!strcmp(path, "/proc/version")) { return proc_emit_literal( "Linux version " GUEST_KERNEL_RELEASE - " (buildd@bos03-arm64-051) " - "(aarch64-linux-gnu-gcc (Ubuntu 15.2.0-4ubuntu4) " - "15.2.0, GNU ld (GNU Binutils for Ubuntu) " - "2.45) " GUEST_KERNEL_VERSION "\n"); + " (elfuse@elfuse) (elfuse) " GUEST_KERNEL_VERSION "\n"); } /* /proc/filesystems, /proc/self/mountinfo, /proc/mounts, /etc/mtab. */ diff --git a/src/syscall/sys.c b/src/syscall/sys.c index 2310dda0..3949497f 100644 --- a/src/syscall/sys.c +++ b/src/syscall/sys.c @@ -40,10 +40,6 @@ static int cached_ngroups = -1; static const linux_utsname_t cached_uname = { .sysname = "Linux", .nodename = "elfuse", - - /* Kernel version: match the lima aarch64 VM kernel to avoid version-gated - * feature detection mismatches in userspace. - */ .release = GUEST_KERNEL_RELEASE, .version = GUEST_KERNEL_VERSION, .machine = "aarch64", diff --git a/src/syscall/sys.h b/src/syscall/sys.h index 2d20ed32..f18faad7 100644 --- a/src/syscall/sys.h +++ b/src/syscall/sys.h @@ -15,11 +15,14 @@ #include #include "core/guest.h" -/* Guest-visible kernel identity, shared by uname(2) and /proc/version so the - * two cannot drift apart. tests/test-proc.c pins them to each other. +/* Guest-visible kernel identity for uname(2) and /proc/version; + * tests/test-proc.c pins the two surfaces to each other. The release names + * the 6.18 LTS baseline and tracks no test image; syscall/dispatch.tbl + * states what elfuse implements. glibc rejects only a release below its + * build-time floor, so a high floor is the safe side. */ -#define GUEST_KERNEL_RELEASE "6.17.0-20-generic" -#define GUEST_KERNEL_VERSION "#20-Ubuntu SMP PREEMPT_DYNAMIC" +#define GUEST_KERNEL_RELEASE "6.18.0" +#define GUEST_KERNEL_VERSION "#1 SMP PREEMPT_DYNAMIC" /* System info syscall handlers. */ diff --git a/tests/test-comprehensive.c b/tests/test-comprehensive.c index c2090c1b..cdd18831 100644 --- a/tests/test-comprehensive.c +++ b/tests/test-comprehensive.c @@ -49,6 +49,15 @@ int main(int argc, char *argv[]) /* x86_64 test binary: uname returns "x86_64" */ CHECK(!strcmp(uts.machine, "x86_64"), "machine == x86_64"); #endif + /* elfuse pins its reported release to the 6.18 LTS baseline. Under the + * qemu reference lane the kernel is a real one and reports its own. + */ + if (!strcmp(uts.nodename, "elfuse")) { + int major = 0, minor = 0; + CHECK(sscanf(uts.release, "%d.%d", &major, &minor) == 2, + "release parses"); + CHECK(major > 6 || (major == 6 && minor >= 18), "release >= 6.18"); + } /* 4. PID */ CHECK(getpid() > 0, "getpid > 0"); From eae4e1f8ea448d1d9f8141da6b2b8c6e086297b5 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Sun, 16 Aug 2026 21:08:21 +0200 Subject: [PATCH 3/5] Document the reported kernel identity No doc named the kernel elfuse claims, the surfaces carrying the claim, or its limits. docs/internals.md gains a Reported Kernel Identity section: the two surfaces and their one definition, the 6.18 LTS rationale, the frozen LINUX_2.6.39 vDSO tag, the calls a real 6.18 provides that elfuse answers with -ENOSYS, and the three things that read the release automatically. dispatch.tbl is the implemented set with two exceptions, so the section names them: pidfd_getfd and userfaultfd are registered there but their handlers return -ENOSYS. The readers are named per lane, since test-proc and test-comprehensive both run under elfuse and under the qemu oracle, and bench-hot-guard-glibc reads the release through glibc's uname fallback without asserting on it. README.md and docs/usage.md each carry one line, since a guest user observes the version directly. --- README.md | 3 +++ docs/internals.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/usage.md | 4 ++++ 3 files changed, 48 insertions(+) diff --git a/README.md b/README.md index 244a31e6..c0f785c8 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,9 @@ do. mask); the host scheduler picks the actual CPU. - `/proc`, `/dev`, and mount data are synthetic compatibility views, not host pass-throughs. +- `uname` and `/proc/version` report Linux 6.18 LTS, a floor for + version-gated userspace; `src/syscall/dispatch.tbl` states what is + implemented. ## License diff --git a/docs/internals.md b/docs/internals.md index a9c3bb9e..2061f72a 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -795,6 +795,47 @@ under `/proc`, `/dev`, and a few Linux-expected compatibility files: - Guest cwd handling preserves a virtual `/proc` working directory even though the host operates on synthetic backing directories. +### Reported Kernel Identity + +Two surfaces tell the guest which kernel it is running on: `uname(2)`, served +from a static `linux_utsname_t` in `src/syscall/sys.c`, and `/proc/version`, +emitted as a literal by `src/runtime/procemu.c`. Both spell the release +through `GUEST_KERNEL_RELEASE` and `GUEST_KERNEL_VERSION` in +`src/syscall/sys.h`, so the two cannot disagree; `tests/test-proc.c` asserts +that the release `uname` reports appears in the `/proc/version` banner. + +The reported release is `6.18.0`, the 6.18 LTS baseline; it tracks no test +image. `elfuse` emits no `/proc/sys/kernel/osrelease`, so glibc's +`dl_discover_osversion` falls back to `uname(2)` and reads this value at +every dynamic startup. It refuses a kernel only when the release is below its +build-time floor, so a high baseline is the safe side. + +The release feeds version-gated feature detection. `src/syscall/dispatch.tbl` +is the implemented set with two exceptions: `pidfd_getfd` and `userfaultfd` +are registered there, but their handlers in `src/syscall/syscall.c` return +`-ENOSYS`, and `tests/test-pidfd.c` and `tests/test-userfaultfd.c` pin them +at that stub. Anything absent from the table gets `-ENOSYS` from the dispatch +default, whatever number `uname` prints. Calls a 6.18 kernel provides that +are absent: `io_uring_*`, `mseal`, `cachestat`, `process_madvise`, +`listmount`/`statmount`, `landlock_*`, and `file_getattr`/`file_setattr`. +Guests probe for a syscall and handle `ENOSYS`. + +The `LINUX_2.6.39` tag in the synthetic vDSO (`src/core/vdso.c`) is the +frozen ELF symbol-version name the real arm64 vDSO exports; a genuine 6.18 +kernel emits it too. + +Three things read the release automatically. `tests/test-proc.c` runs on both +the elfuse and the qemu aarch64 lane and checks only that the banner and +`uname` agree, so on the qemu lane it pins the real Alpine kernel to itself. +`tests/test-comprehensive.c` asserts the release is at least 6.18 behind a +`uts.nodename == "elfuse"` gate, which holds on the elfuse lanes and not on +the qemu lane, whose initramfs hostname is `elfuse-qemu`. The third reader is +silent: `build/bench-hot-guard-glibc` is the one aarch64 glibc binary in +`make check`, built only when the cross-toolchain sysroot is present, and a +release below glibc's floor would abort its startup rather than fail an +assertion. The Alpine tree `tests/fetch-fixtures.sh` builds is musl +throughout and does no version gating. + `/proc/self/smaps` and `/proc//smaps` are generated from the same tracked VMA list as `/proc/self/maps`. The complete field set currently emitted for each VMA is the maps header followed by these 24 fields, in this order: diff --git a/docs/usage.md b/docs/usage.md index 561e9f0d..2ec20943 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -287,6 +287,10 @@ from targeted ABI translation and emulation at the syscall boundary. That has a few direct implications: - `/proc` and `/dev` are compatibility surfaces, not passthrough mounts. +- `uname` and `/proc/version` report Linux 6.18 LTS, a stable floor for + version-gated feature detection; the implemented syscall set is + `src/syscall/dispatch.tbl`. See [internals.md](internals.md), section + "Reported Kernel Identity". - macOS and Linux file, socket, and signal semantics are normalized in the host syscall layer. - Behavior is strongest for normal command-line tools, language runtimes, test From 1442b3ca60ee738940c8cf734f7b3f15c656d7f4 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Sun, 16 Aug 2026 21:57:02 +0200 Subject: [PATCH 4/5] Boot the qemu oracle on a 6.18 kernel The oracle ran Alpine 3.21's linux-virt 6.12 while elfuse reports 6.18, so the differential lanes compared against a kernel two LTS lines behind the claim. linux-virt now resolves from the Alpine 3.23 main repo, which ships 6.18; the kernel and its lib/modules tree come from that one apk, so the 9p and virtio-net modules the boot init loads match the kernel. The 37 userland pins stay on 3.21. Verified: qemu_exec uname -r prints 6.18.44-0-virt, the 9p mount holds, and both matrix lanes stay at their baselines. --- tests/fetch-fixtures.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/fetch-fixtures.sh b/tests/fetch-fixtures.sh index 7fbcba0b..e83489a1 100755 --- a/tests/fetch-fixtures.sh +++ b/tests/fetch-fixtures.sh @@ -23,6 +23,10 @@ # # Environment: # FORCE=1 Rebuild every stage from scratch. +# KERNEL_ALPINE_VERSION= +# Alpine release the oracle kernel comes from, default +# 3.23 (Linux 6.18 LTS). Userland stays on +# ALPINE_VERSION. # INCLUDE_X86_64=1 Also fetch x86_64 userspace for the elfuse-x86_64 # test-matrix mode. Default off; adds ~80 MiB of # downloads but reuses the same Alpine version pin @@ -36,6 +40,12 @@ set -euo pipefail . "$(dirname "$0")/lib/bash-compat.sh" ALPINE_VERSION="${ALPINE_VERSION:-3.21}" + +# The oracle kernel resolves from its own Alpine release: 3.23 ships +# linux-virt 6.18, the LTS line elfuse reports to the guest, while the +# userland pins stay on ALPINE_VERSION. The linux-virt apk carries the +# kernel and its lib/modules tree, so both move together. +KERNEL_ALPINE_VERSION="${KERNEL_ALPINE_VERSION:-3.23}" # Empty by default -- the exact point release is resolved from the live releases # listing at fetch time (see resolve_minirootfs), then written back here so the # x86_64 path reuses the same patch. Set explicitly to pin one. @@ -46,6 +56,7 @@ CDN_BASE="https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}" RELEASES="${CDN_BASE}/releases/${ALPINE_ARCH}" MAIN_REPO="${CDN_BASE}/main/${ALPINE_ARCH}" COMMUNITY_REPO="${CDN_BASE}/community/${ALPINE_ARCH}" +KERNEL_REPO="https://dl-cdn.alpinelinux.org/alpine/v${KERNEL_ALPINE_VERSION}/main/${ALPINE_ARCH}" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" FIXTURES="${REPO_ROOT}/externals/test-fixtures" @@ -65,7 +76,7 @@ INITRAMFS="${FIXTURES}/initramfs.cpio.gz" # Tuples (not associative arrays) keep this working on bash 3.2 hosts (stock # macOS /bin/bash) -- see tests/lib/bash-compat.sh. PKGS=( - "main:linux-virt" + "kernel:linux-virt" "main:busybox-static" "main:dropbear" "main:zlib" @@ -189,6 +200,7 @@ apk_url() case "$repo" in main) echo "${MAIN_REPO}/${name}-${version}.apk" ;; community) echo "${COMMUNITY_REPO}/${name}-${version}.apk" ;; + kernel) echo "${KERNEL_REPO}/${name}-${version}.apk" ;; *) echo "unknown repo: $repo" >&2 return 1 @@ -207,6 +219,7 @@ repo_url() case "$1" in main) echo "$MAIN_REPO" ;; community) echo "$COMMUNITY_REPO" ;; + kernel) echo "$KERNEL_REPO" ;; *) echo "unknown repo: $1" >&2 return 1 @@ -384,7 +397,7 @@ main() # Extract just the kernel-modules subtree from linux-virt. local modstage linux_virt_ver - linux_virt_ver="$(pkg_version "main:linux-virt")" + linux_virt_ver="$(pkg_version "kernel:linux-virt")" modstage="$(mktemp -d)" tar xzf "$(apk_path linux-virt "$linux_virt_ver")" \ -C "$modstage" 'lib/modules' 2> /dev/null @@ -472,7 +485,7 @@ EOF if [ ! -s "${KERNEL_DIR}/vmlinuz-virt" ] || [ "${FORCE:-0}" = "1" ] || [ "$REBUILD" = 1 ]; then log "extract kernel" local linux_virt_ver - linux_virt_ver="$(pkg_version "main:linux-virt")" + linux_virt_ver="$(pkg_version "kernel:linux-virt")" rm -rf "${KERNEL_DIR}/work" mkdir -p "${KERNEL_DIR}/work" tar xzf "$(apk_path linux-virt "$linux_virt_ver")" \ From f54c034af4e054b0888bbe154d4221e24ccb2f40 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Sun, 16 Aug 2026 21:57:46 +0200 Subject: [PATCH 5/5] State captured VM values as provenance Three comments assert a live correspondence with a lima VM that exists nowhere in the repo: the sysinfo RAM cap says "Match Lima VZ 4GiB VM", rosetta.h says the probe values "match what the Lima-on-VZ Linux VM observes", and proc.c restates its own provenance sentence as "matching a real VZ (Lima) VM". The values are captured data; the comments now say so and stop implying a sync obligation. tests/test-poll.c stops naming lima as a lane the matrix does not have; the setpgid variance is session-leader status, which follows the launcher. --- src/core/rosetta.h | 4 ++-- src/syscall/proc.c | 4 ++-- src/syscall/sys.c | 3 ++- tests/test-poll.c | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/rosetta.h b/src/core/rosetta.h index eff3719e..05dff302 100644 --- a/src/core/rosetta.h +++ b/src/core/rosetta.h @@ -51,8 +51,8 @@ * VZ environment. Without affirmative responses, rosetta prints "Rosetta is * only intended to run on Apple Silicon ..." and exits. * - * Reverse-engineered from the rosetta binary; values match what the Lima-on-VZ - * Linux VM observes via strace. + * Reverse-engineered from the rosetta binary; values captured via strace in + * a VZ Linux VM. */ #define ROSETTA_VZ_CHECK 0x80456125 /* Returns 69-byte signature */ #define ROSETTA_VZ_CAPS 0x80806123 /* Returns 128-byte capability blob */ diff --git a/src/syscall/proc.c b/src/syscall/proc.c index 3bb9f46b..48ef92e6 100644 --- a/src/syscall/proc.c +++ b/src/syscall/proc.c @@ -3169,8 +3169,8 @@ static void vcpu_handle_mrs_trap(hv_vcpu_t vcpu, uint64_t value = 0; - /* ID register emulation: return VZ-sanitized values matching a real VZ - * (Lima) VM BEFORE trying HVF. HVF's hv_vcpu_get_sys_reg succeeds for ID + /* ID register emulation: return VZ-sanitized values BEFORE trying HVF. + * HVF's hv_vcpu_get_sys_reg succeeds for ID * registers but returns raw hardware values, which include features the * hypervisor does not actually virtualize. * diff --git a/src/syscall/sys.c b/src/syscall/sys.c index 3949497f..75908903 100644 --- a/src/syscall/sys.c +++ b/src/syscall/sys.c @@ -104,7 +104,8 @@ static void sysinfo_init_cached_host_state(void) size_t ms_len = sizeof(memsize); int mib_mem[2] = {CTL_HW, HW_MEMSIZE}; if (sysctl(mib_mem, 2, &memsize, &ms_len, NULL, 0) == 0) { - const uint64_t vm_ram_cap = 4094595072ULL; /* Match Lima VZ 4GiB VM */ + const uint64_t vm_ram_cap = + 4094595072ULL; /* totalram cap captured from a 4 GiB VZ VM */ cached_real_memsize = memsize; cached_totalram = (memsize > vm_ram_cap) ? vm_ram_cap : memsize; } diff --git a/tests/test-poll.c b/tests/test-poll.c index 46453fcb..5363dfbb 100644 --- a/tests/test-poll.c +++ b/tests/test-poll.c @@ -398,7 +398,8 @@ int main(void) if (setpgid(0, 0) == -1 && errno == EPERM) PASS(); else if (setpgid(0, 0) == 0) - PASS(); /* Also acceptable (lima/native may behave differently) */ + PASS(); /* not the session leader when a launcher owns the session + */ else FAIL("setpgid unexpected result"); }