From e5677064e68d3fb7119a74f31db924aecc3e72e3 Mon Sep 17 00:00:00 2001 From: Xalestar Date: Mon, 17 Aug 2026 15:12:22 +0800 Subject: [PATCH] Stop capping the memory total the guest sees sysinfo(2) caps totalram at a hardcoded 4094595072 bytes while /proc/meminfo reports sysctl(HW_MEMSIZE), so the two surfaces describe different machines: 3998628 kB against 25165824 kB on a 24 GiB host. A guest that reads both subtracts one from the other. busybox free takes total and free from sysinfo and buff/cache from /proc/meminfo and computes used = total - free - cached; a Cached larger than the whole of totalram wraps that subtraction, and free prints the total and a used of 18014398500886155 run together into a single 24-digit field: Mem: 399862818014398500886155 257465 0 ... Nothing outside sys.c reads the ceiling and no test pins it, so deleting it is the shortest route to agreement: both surfaces take the total from sys_guest_ram_bytes(), which is HW_MEMSIZE. That also retires the proportional scaling the ceiling required, and a 64-bit overflow inside it where scaling in bytes multiplied the host figure by the capped total and left 64 bits once host free memory reached about 4.2 GiB. A guest sizing a heap or a job count from MemTotal reads the machine it runs on rather than a sixth of it. Free has to be reported once as well: busybox subtracts sysinfo's freeram rather than MemFree, and the two are separate host_statistics64 samples that drift against each other. sys_guest_ram_free() serves both from the snapshot sysinfo reports. The Mach counters do not partition. A purgeable page is also counted in inactive, so free plus inactive plus purgeable can exceed physical memory whatever the total is, and scaling or not scaling makes no difference to that. proc_open_meminfo() clamps Cached against what MemFree leaves, which is the invariant the guest's subtraction needs. Cached gives up the precision rather than MemFree, since MemFree has to keep matching freeram. The test asserts the agreement rather than either number, since the totals are host-dependent, and it runs in test-matrix against a real kernel where both come from one si_meminfo() and agree by construction. --- src/runtime/procemu.c | 48 ++++++++++++------ src/syscall/sys.c | 114 +++++++++++++++++++++++++----------------- src/syscall/sys.h | 17 +++++++ tests/test-sysinfo.c | 64 ++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 62 deletions(-) diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index 8215504d..6f051aa6 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -1995,31 +1995,37 @@ static int proc_open_self_smaps(const guest_t *g) return proc_finish_maps_output(result, &entries, &builder); } -/* Emit /proc/meminfo from host sysctl (HW_MEMSIZE) plus mach vm_statistics64, - * approximating the Linux fields macOS does not expose. +/* Emit /proc/meminfo from the guest-visible memory figures plus mach + * vm_statistics64, approximating the Linux fields macOS does not expose. + * + * MemTotal and MemFree come from sys_guest_ram_bytes()/sys_guest_ram_free() + * rather than from a host sample taken here, so this file and sysinfo(2) read + * one snapshot rather than two: a guest reading both back to back, as busybox + * free does, sees figures that were measured together. They used to describe + * two different machines outright, never mind two instants -- meminfo reported + * the host's real memory while sysinfo capped it at a hardcoded 4GiB -- and + * that guest underflowed. busybox free takes total and free from sysinfo and + * buff/cache from here, so a Cached larger than the sysinfo total made + * used = total - free - cached wrap into a 24-digit figure. * * Returns a host fd, or -1. Split out of proc_intercept_open to keep that * dispatcher readable. */ static int proc_open_meminfo(void) { - int64_t physmem = 0; - size_t sz = sizeof(physmem); - int mib[2] = {CTL_HW, HW_MEMSIZE}; - sysctl(mib, 2, &physmem, &sz, NULL, 0); - uint64_t total_kb = (uint64_t) physmem / 1024; + uint64_t total_kb = sys_guest_ram_bytes() / 1024; + uint64_t free_kb = sys_guest_ram_free() / 1024; - /* Query host vm_statistics for accurate free/active/inactive. Falls back to + /* Query host vm_statistics for accurate active/inactive. Falls back to * approximations if the mach call fails. */ - uint64_t free_kb, avail_kb, buffers_kb, cached_kb; + uint64_t avail_kb, buffers_kb, cached_kb; vm_statistics64_data_t vm_stat = {0}; mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; uint64_t page_size = 4096; if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t) &vm_stat, &count) == KERN_SUCCESS) { host_page_size(mach_host_self(), (vm_size_t *) &page_size); - free_kb = (uint64_t) vm_stat.free_count * page_size / 1024; uint64_t inactive_kb = (uint64_t) vm_stat.inactive_count * page_size / 1024; uint64_t purgeable_kb = @@ -2031,18 +2037,30 @@ static int proc_open_meminfo(void) cached_kb = inactive_kb + purgeable_kb; buffers_kb = 0; /* macOS does not expose buffer cache separately */ } else { - free_kb = total_kb / 2; avail_kb = total_kb * 3 / 4; buffers_kb = total_kb / 20; cached_kb = total_kb / 4; } - /* Saturating subtraction. On macOS free + cached (inactive + purgeable) can - * exceed physical total, which would unsigned-underflow these derived - * fields into absurd values. + /* Cached has to fit in what MemFree leaves, not merely be smaller than the + * total. The Mach counters do not partition: a purgeable page is also + * counted in inactive, so free + inactive + purgeable can exceed physical + * memory however the total is derived. A guest computing + * used = total - free - cached wraps on that excess, which is the failure + * reporting one total is meant to end. Cached gives up the precision rather + * than MemFree, since MemFree is what sysinfo reports as freeram and the + * two have to keep matching. free_kb needs no clamp of its own: it comes + * from sys_guest_ram_free(), which is already held below the total. + */ + if (cached_kb > total_kb - free_kb) + cached_kb = total_kb - free_kb; + + /* Active is now an exact remainder -- the clamp above makes free + cached + * fit inside the total. AnonPages still saturates, because Buffers is not + * part of that clamp and the fallback branch above sets it non-zero. */ uint64_t fc_kb = free_kb + cached_kb; - uint64_t active_kb = total_kb > fc_kb ? total_kb - fc_kb : 0; + uint64_t active_kb = total_kb - fc_kb; uint64_t anon_kb = total_kb > fc_kb + buffers_kb ? total_kb - fc_kb - buffers_kb : 0; diff --git a/src/syscall/sys.c b/src/syscall/sys.c index 75908903..f0d268d4 100644 --- a/src/syscall/sys.c +++ b/src/syscall/sys.c @@ -47,6 +47,14 @@ static const linux_utsname_t cached_uname = { }; static const uint8_t cached_affinity_mask[256] = {1}, zero_block[256] = {0}; +/* Physical memory to report when sysctl(HW_MEMSIZE) fails. Reporting zero is + * not an option: a guest computing used = total - free - cached against a zero + * total wraps, which is the failure sys_guest_ram_bytes() exists to prevent. + * The figure is one captured from a 4 GiB VZ VM, so the fallback describes a + * machine that plausibly exists. + */ +#define GUEST_RAM_FALLBACK_BYTES 4094595072ULL + /* sysinfo cache. * * Process-scoped by intent: the cache mirrors the host's view (totalram from @@ -59,7 +67,7 @@ static const uint8_t cached_affinity_mask[256] = {1}, zero_block[256] = {0}; static pthread_once_t sysinfo_once = PTHREAD_ONCE_INIT; static pthread_rwlock_t sysinfo_lock = PTHREAD_RWLOCK_INITIALIZER; static time_t cached_boottime_sec = 0; -static uint64_t cached_totalram = 0, cached_real_memsize = 0; +static uint64_t cached_totalram = 0; static uint64_t cached_page_size = 0; static mach_port_t cached_host_port = MACH_PORT_NULL; static time_t cached_sysinfo_sec = -1; @@ -103,12 +111,9 @@ static void sysinfo_init_cached_host_state(void) uint64_t memsize = 0; 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; /* totalram cap captured from a 4 GiB VZ VM */ - cached_real_memsize = memsize; - cached_totalram = (memsize > vm_ram_cap) ? vm_ram_cap : memsize; - } + if (sysctl(mib_mem, 2, &memsize, &ms_len, NULL, 0) != 0 || memsize == 0) + memsize = GUEST_RAM_FALLBACK_BYTES; + cached_totalram = memsize; long page_size = sysconf(_SC_PAGESIZE); if (page_size > 0) @@ -127,24 +132,18 @@ static void sysinfo_refresh_cached_locked(time_t now_sec) if (cached_boottime_sec != 0) cached_sysinfo.uptime = now_sec - cached_boottime_sec; - /* Free RAM from vm_statistics64. Scale proportionally if totalram is - * capped. + /* Free RAM from vm_statistics64, held below the total unconditionally so + * that every reader of the pair -- here and /proc/meminfo -- can subtract + * one from the other without checking. */ vm_statistics64_data_t vmstat = {0}; mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; if (cached_host_port != MACH_PORT_NULL && host_statistics64(cached_host_port, HOST_VM_INFO64, (host_info64_t) &vmstat, &count) == KERN_SUCCESS) { - uint64_t real_free = (uint64_t) vmstat.free_count * cached_page_size; - if (cached_real_memsize > 0 && - cached_real_memsize > cached_sysinfo.totalram) { - uint64_t scaled_free = real_free; - scaled_free *= cached_sysinfo.totalram; - scaled_free /= cached_real_memsize; - cached_sysinfo.freeram = scaled_free; - } else { - cached_sysinfo.freeram = real_free; - } + uint64_t free_bytes = (uint64_t) vmstat.free_count * cached_page_size; + cached_sysinfo.freeram = + free_bytes > cached_totalram ? cached_totalram : free_bytes; } /* Load averages (x 65536 for fixed-point). */ @@ -158,6 +157,50 @@ static void sysinfo_refresh_cached_locked(time_t now_sec) cached_sysinfo_sec = now_sec; } +/* Copy out the cached sysinfo, refreshing it first if it has aged past a + * second. Every guest-visible memory figure comes through here, so that a guest + * reading two surfaces back to back reads one snapshot rather than two samples. + */ +static void sysinfo_snapshot(linux_sysinfo_t *out) +{ + pthread_once(&sysinfo_once, sysinfo_init_cached_host_state); + time_t now_sec = time(NULL); + + if (thread_is_single_active()) { + if (cached_sysinfo_sec != now_sec) + sysinfo_refresh_cached_locked(now_sec); + *out = cached_sysinfo; + return; + } + + pthread_rwlock_rdlock(&sysinfo_lock); + if (cached_sysinfo_sec == now_sec) { + *out = cached_sysinfo; + pthread_rwlock_unlock(&sysinfo_lock); + return; + } + pthread_rwlock_unlock(&sysinfo_lock); + + pthread_rwlock_wrlock(&sysinfo_lock); + if (cached_sysinfo_sec != now_sec) + sysinfo_refresh_cached_locked(now_sec); + *out = cached_sysinfo; + pthread_rwlock_unlock(&sysinfo_lock); +} + +uint64_t sys_guest_ram_bytes(void) +{ + pthread_once(&sysinfo_once, sysinfo_init_cached_host_state); + return cached_totalram; +} + +uint64_t sys_guest_ram_free(void) +{ + linux_sysinfo_t si; + sysinfo_snapshot(&si); + return si.freeram; /* mem_unit is 1, so freeram is already bytes */ +} + static int get_cached_linux_groups(void) { if (thread_is_single_active()) { @@ -543,34 +586,11 @@ int64_t sys_getrusage(guest_t *g, int who, uint64_t usage_gva) int64_t sys_sysinfo(guest_t *g, uint64_t info_gva) { - pthread_once(&sysinfo_once, sysinfo_init_cached_host_state); - time_t now_sec = time(NULL); - - if (thread_is_single_active()) { - if (cached_sysinfo_sec != now_sec) - sysinfo_refresh_cached_locked(now_sec); - if (guest_write_small(g, info_gva, &cached_sysinfo, - sizeof(cached_sysinfo)) < 0) - return -LINUX_EFAULT; - return 0; - } else { - linux_sysinfo_t si; - pthread_rwlock_rdlock(&sysinfo_lock); - if (cached_sysinfo_sec == now_sec) { - si = cached_sysinfo; - pthread_rwlock_unlock(&sysinfo_lock); - } else { - pthread_rwlock_unlock(&sysinfo_lock); - pthread_rwlock_wrlock(&sysinfo_lock); - if (cached_sysinfo_sec != now_sec) - sysinfo_refresh_cached_locked(now_sec); - si = cached_sysinfo; - pthread_rwlock_unlock(&sysinfo_lock); - } - if (guest_write_small(g, info_gva, &si, sizeof(si)) < 0) - return -LINUX_EFAULT; - return 0; - } + linux_sysinfo_t si; + sysinfo_snapshot(&si); + if (guest_write_small(g, info_gva, &si, sizeof(si)) < 0) + return -LINUX_EFAULT; + return 0; } /* Resource limits. */ diff --git a/src/syscall/sys.h b/src/syscall/sys.h index f18faad7..1dcfc6c7 100644 --- a/src/syscall/sys.h +++ b/src/syscall/sys.h @@ -60,6 +60,23 @@ int64_t sys_prlimit64(guest_t *g, uint64_t new_gva, uint64_t old_gva); +/* Guest-visible physical memory, shared by every surface that reports it. + * + * A guest that reads two surfaces subtracts one against the other, and there + * the underflow happens beyond any saturating arithmetic elfuse does on its own + * side. busybox free is exactly this reader: it takes total and free from + * sysinfo(2) and buff/cache from /proc/meminfo, then computes + * used = total - free - cached. So the total and the free both have to come + * from here rather than being sampled once per surface. + * + * sys_guest_ram_bytes() is the total to report as MemTotal/totalram: the host's + * real memory, or a plausible stand-in if the sysctl fails, never zero. + * sys_guest_ram_free() is the matching free figure, taken from the same cached + * snapshot sysinfo(2) reports and guaranteed not to exceed the total. + */ +uint64_t sys_guest_ram_bytes(void); +uint64_t sys_guest_ram_free(void); + /* Snapshot/restore the guest-visible RLIMIT_NOFILE across the posix_spawn * fork boundary. The host limit is internal capacity and is deliberately not * exposed through these helpers. diff --git a/tests/test-sysinfo.c b/tests/test-sysinfo.c index a26f0216..76c955aa 100644 --- a/tests/test-sysinfo.c +++ b/tests/test-sysinfo.c @@ -33,6 +33,70 @@ int main(void) FAIL("sysinfo failed"); } + /* sysinfo(2) and /proc/meminfo must describe the same machine. + * + * A guest that reads both subtracts one against the other: busybox free + * takes total and free from sysinfo and buff/cache from /proc/meminfo, then + * computes used = total - free - cached. While elfuse capped sysinfo's + * total at a hardcoded 4GiB but let /proc/meminfo report the host's real + * memory, Cached exceeded the sysinfo total and that subtraction wrapped, + * printing a 24-digit "used". The wrap happens in the guest, past any + * saturating arithmetic elfuse does on its own side, so the surfaces have + * to agree here. + * + * The second assertion is the one busybox actually performs, so it is + * stated in sysinfo's terms rather than MemFree's: freeram is what gets + * subtracted, and it is a different read from the MemFree in this file. + * Their exact equality is deliberately not asserted -- under a real kernel + * the two reads are separate samples of a moving figure, and elfuse only + * promises they come from one snapshot, not that no snapshot ever changes. + * + * Host-dependent by nature: the totals are whatever the machine has, so the + * test asserts their agreement rather than any particular figure. Under a + * real kernel both come from one si_meminfo() and agree by construction, + * which is what the test-matrix column checks. Under the pre-fix elfuse the + * first assertion fails on any host with more than 4GiB of RAM, and passes + * vacuously below that. + */ + TEST("sysinfo agrees with /proc/meminfo"); + { + struct sysinfo si; + if (sysinfo(&si) != 0) + FAIL("sysinfo failed"); + else { + FILE *f = fopen("/proc/meminfo", "r"); + if (!f) + FAIL("could not open /proc/meminfo"); + else { + unsigned long long total_kb = 0, free_kb = 0, cached_kb = 0; + int seen = 0; + char line[256]; + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "MemTotal: %llu kB", &total_kb) == 1) + seen |= 1; + else if (sscanf(line, "MemFree: %llu kB", &free_kb) == 1) + seen |= 2; + else if (sscanf(line, "Cached: %llu kB", &cached_kb) == 1) + seen |= 4; + } + fclose(f); + + unsigned long long si_total_kb = + (unsigned long long) si.totalram * si.mem_unit / 1024; + unsigned long long si_free_kb = + (unsigned long long) si.freeram * si.mem_unit / 1024; + + if (seen != 7) + FAIL("MemTotal/MemFree/Cached missing from /proc/meminfo"); + else + EXPECT_TRUE(si_total_kb == total_kb && + si_free_kb + cached_kb <= si_total_kb && + free_kb + cached_kb <= total_kb, + "sysinfo and /proc/meminfo disagree"); + } + } + } + /* Test getrusage */ TEST("getrusage"); {