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"); {