$RodHat_
MOTD

vsyscall emulation is finally gone. Yes, this breaks your 2006 binary.

Published by

vsyscall emulation is finally gone. Yes, this breaks your 2006 binary.
Photo: AI-generated — no human photographer / RodHat AI Cover

Linux 6.23 shipped the week of October 6. The headline on most coverage was the scheduler work and the Rust networking driver. Nobody mentioned the removal of CONFIG_LEGACY_VSYSCALL_EMULATE. Nobody ever mentions it, until someone’s deploy breaks.

The option is gone. The emulation is gone. If you have a static binary from 2006 that calls vsyscall directly, you will find out about it in the most entertaining way.

What vsyscall was

x86_64 got vsyscall in 2001. The problem it solved was real: gettimeofday and clock_gettime are hot paths. Calling into the kernel on every timestamp read hurt. The original kernel syscall instruction costs a privilege-level switch and a TLB flush on some microarchitectures. For something you call ten thousand times a second, that adds up.

The vsyscall mechanism solved this by mapping a kernel-owned page at a fixed address in every process’s address space: 0xffffffffff600000. That page contained a handful of fast-path implementations for gettimeofday, time, and getcpu. Userspace called it like a function, no syscall instruction required. The kernel kept a shared data region updated with the current time, and the vsyscall code just read from it.

It worked. It was fast. It was also a fixed-address executable mapping in every process on the system, and that made it a reliable code-reuse gadget for exploits. You knew, on every kernel, exactly where to find executable code that would give you gettimeofday. ROP chains loved it.

The vDSO replaced it

The virtual Dynamic Shared Object landed in the kernel in 2003 for 32-bit and was extended to x86_64 shortly after. The vDSO does the same thing vsyscall did, with the critical difference that the kernel maps it at a randomized address per process. ASLR applies. There is no fixed gadget for an attacker to target.

By 2008, glibc used vDSO exclusively for gettimeofday and friends. The only code that still calls vsyscall directly at this point is hand-rolled assembly that explicitly computed 0xffffffffff600000 and jumped there, or very old statically linked binaries built against a pre-vDSO glibc.

So the kernel added vsyscall=none as a boot option years ago. Security- hardened distributions and container runtimes have been shipping with it for a decade. If your code was going to break, it broke in 2015 when someone finally turned it off.

What CONFIG_LEGACY_VSYSCALL_EMULATE actually did

There were three vsyscall modes after the security work: native (original behavior, fixed executable page), emulate (the same fixed address, but mapped as a non-executable page that causes a fault, which the kernel catches and emulates in the page fault handler), and none (no mapping at all, instant SIGSEGV for anything that tries to call it).

CONFIG_LEGACY_VSYSCALL_EMULATE enabled the emulate mode. You got compatibility for old binaries without the full exploit-gadget of a live executable page. The process took a fault and the kernel handled it, which is slow, but it worked.

Linux 6.23 removes the config option. Distros that don’t set vsyscall=none explicitly will now get none by default, because there is nothing left to configure. emulate does not exist anymore.

What breaks

Probably nothing you run today. Possibly something you run that you forgot you run.

Static binaries are the risk. A dynamically linked binary uses the system glibc, which switched to vDSO in 2008. A static binary carries its own libc at compile time. If someone built that static binary against a glibc from before 2008 and never recompiled it, and it calls gettimeofday in a tight loop, it calls vsyscall, and it will segfault.

The other category: hand-rolled assembly or system call wrappers in old C code that explicitly computed the vsyscall address as a constant. This code exists. It is mostly in legacy finance and embedded tooling from the 2004-2009 era. If you have something like this, you know who wrote it and you have not been able to retire it for reasons that would take an hour to explain.

Container images are the hidden surface. A container carries its own userspace binaries. If someone built a container image off a 2010 base and never updated it, and that image runs on a 6.23 kernel, the userspace calls vsyscall, the kernel has no emulation, and the process dies. The kernel and container userspace have decoupled lifetimes. This decoupling bites.

What the fix is

For dynamically linked binaries: recompile against any glibc from the last fifteen years. You are done. The vDSO handles everything.

For ancient static binaries: you need to rebuild them against a current static libc. musl is the sane choice for new static builds. glibc static linking still works. Either one will produce a binary that uses the vDSO. If rebuilding from source is not possible because the source is gone, you have a binary you cannot repair and you need to run it in a VM with an older kernel, not on your fleet.

For the 0xffffffffff600000 hardcoded-address code: rewrite it. It was wrong in 2010 and it is still wrong now.

Check your container images. Run objdump -d <binary> | grep 'ffffffffff600' or strace <binary> 2>&1 | grep vsyscall on anything suspicious before you find out the hard way.

The vDSO mechanism is documented in vdso(7) and the vDSO tip post covers how to use it from userspace if you are writing code that talks to it directly. The mseal post covers the other recent change to the x86_64 memory layout story.

Sources

  1. x86_64 virtual system calls (vsyscall), Linux kernel documentation (The Linux Kernel Project)
  2. vdso(7): overview of the virtual ELF dynamic shared object (Linux man-pages project)