Skip to main content

c/
vdso.rs

1// Copyright (c) 2026 Meng Zhuo
2// SPDX-License-Identifier: BSD-3-Clause
3
4// ============================================================
5// vDSO fast paths
6// ============================================================
7//
8// The kernel maps a small ELF image (AT_SYSINFO_EHDR) into every process.
9// It exports clock_gettime, gettimeofday, getcpu and, on recent kernels,
10// getrandom.  Calling them avoids a ring transition; musl does the same for
11// clock_gettime and getcpu.  Every entry point here falls back to the real
12// syscall when the symbol or the kernel support is missing.
13//
14// The vDSO uses two naming schemes.  x86_64 and riscv64 export
15// __vdso_<name>; aarch64 exports __kernel_<name>.  Look both up and cache
16// whichever one the current kernel provides.
17
18const AT_SYSINFO_EHDR: c_ulong = 33;
19const AT_PAGESZ: c_ulong = 6;
20
21const PT_LOAD: u32 = 1;
22const PT_DYNAMIC: u32 = 2;
23const DT_HASH: u64 = 4;
24const DT_STRTAB: u64 = 5;
25const DT_SYMTAB: u64 = 6;
26
27const STT_FUNC: u8 = 2;
28const STB_GLOBAL: u8 = 1;
29const STB_WEAK: u8 = 2;
30
31const MADV_WIPEONFORK: i32 = 18;
32
33// Opaque per-thread state for vDSO getrandom.  One pointer lives in the
34// otherwise unused part of the 256-byte TCB (0x38; errno ends at 0x34).
35const GETRANDOM_TCB_OFFSET: usize = 0x38;
36
37// The state pointer uses 1 as an "allocation in progress" marker so a signal
38// handler that re-enters getrandom falls back to the syscall instead of
39// allocating a second state.
40const STATE_ALLOCATING: usize = 1;
41
42// vgetrandom_opaque_params from linux/random.h.
43#[repr(C)]
44struct VgetrandomParams {
45    size_of_opaque_state: u32,
46    mmap_prot: u32,
47    mmap_flags: u32,
48    reserved: [u32; 13],
49}
50
51const VGETRANDOM_UNINIT: usize = 0;
52const VGETRANDOM_INITIALIZING: usize = 1;
53const VGETRANDOM_READY: usize = 2;
54const VGETRANDOM_DISABLED: usize = 3;
55
56static VGETRANDOM_STATE: AtomicUsize = AtomicUsize::new(VGETRANDOM_UNINIT);
57static VGETRANDOM_FN: AtomicUsize = AtomicUsize::new(0);
58static VGETRANDOM_SIZE: AtomicUsize = AtomicUsize::new(0);
59static VGETRANDOM_PROT: AtomicUsize = AtomicUsize::new(0);
60static VGETRANDOM_FLAGS: AtomicUsize = AtomicUsize::new(0);
61static VGETRANDOM_PAGE: AtomicUsize = AtomicUsize::new(0);
62
63// Serializes first-time state allocation.  try_lock semantics are deliberate:
64// a signal handler that re-enters getrandom while its own thread is
65// allocating must fall back to the syscall, not spin forever.
66static VGETRANDOM_ALLOC: AtomicUsize = AtomicUsize::new(0);
67
68// usize::MAX means "not resolved yet"; 0 means "no vDSO".
69static CLOCK_GETTIME_VDSO: AtomicUsize = AtomicUsize::new(usize::MAX);
70
71// The getcpu cache starts as a callable resolver so the hot path only has to
72// test for NULL, matching musl's lazy vDSO pointer trick.
73static GETCPU_VDSO: core::sync::atomic::AtomicPtr<c_void> =
74    core::sync::atomic::AtomicPtr::new(getcpu_init as *mut c_void);
75
76#[inline]
77unsafe fn read_u16(p: *const u8) -> u16 {
78    u16::from_le(core::ptr::read_unaligned(p as *const u16))
79}
80
81#[inline]
82unsafe fn read_u32(p: *const u8) -> u32 {
83    u32::from_le(core::ptr::read_unaligned(p as *const u32))
84}
85
86#[inline]
87unsafe fn read_u64(p: *const u8) -> u64 {
88    u64::from_le(core::ptr::read_unaligned(p as *const u64))
89}
90
91unsafe fn cstr_eq(s: *const u8, want: &[u8]) -> bool {
92    let mut i = 0usize;
93    while i < want.len() {
94        if *s.add(i) != want[i] {
95            return false;
96        }
97        i += 1;
98    }
99    *s.add(want.len()) == 0
100}
101
102/// Look up a symbol in the vDSO by name.  Versions are ignored: the vDSO
103/// exports exactly one definition of each entry point.
104unsafe fn vdso_lookup(want: &[u8]) -> usize {
105    let eh = crate::getauxval(AT_SYSINFO_EHDR) as usize;
106    if eh == 0 {
107        return 0;
108    }
109    let image = eh as *const u8;
110    if read_u32(image) != 0x464c_457f || *image.add(4) != 2 {
111        return 0; // not a 64-bit ELF image
112    }
113    let phoff = read_u64(image.add(32)) as usize;
114    let phentsize = read_u16(image.add(54)) as usize;
115    let phnum = read_u16(image.add(56)) as usize;
116
117    let mut load_base = usize::MAX;
118    let mut dynamic = 0usize;
119    let mut i = 0usize;
120    while i < phnum {
121        let ph = image.add(phoff + i * phentsize);
122        match read_u32(ph) {
123            PT_LOAD => {
124                let off = read_u64(ph.add(8)) as usize;
125                let vaddr = read_u64(ph.add(16)) as usize;
126                load_base = eh.wrapping_add(off).wrapping_sub(vaddr);
127            }
128            PT_DYNAMIC => {
129                dynamic = eh.wrapping_add(read_u64(ph.add(8)) as usize);
130            }
131            _ => {}
132        }
133        i += 1;
134    }
135    if load_base == usize::MAX || dynamic == 0 {
136        return 0;
137    }
138
139    let mut strtab = 0usize;
140    let mut symtab = 0usize;
141    let mut hash = 0usize;
142    let mut dp = dynamic as *const u8;
143    loop {
144        let tag = read_u64(dp);
145        if tag == 0 {
146            break;
147        }
148        let val = read_u64(dp.add(8)) as usize;
149        match tag {
150            DT_STRTAB => strtab = load_base.wrapping_add(val),
151            DT_SYMTAB => symtab = load_base.wrapping_add(val),
152            DT_HASH => hash = load_base.wrapping_add(val),
153            _ => {}
154        }
155        dp = dp.add(16);
156    }
157    if strtab == 0 || symtab == 0 || hash == 0 {
158        return 0;
159    }
160
161    // DT_HASH starts with nbucket/nchain; the vDSO always carries one.
162    let nsym = read_u32((hash + 4) as *const u8) as usize;
163    let mut i = 1usize;
164    while i < nsym {
165        let sym = (symtab + i * 24) as *const u8;
166        let info = *sym.add(4);
167        if info & 0xf != STT_FUNC {
168            i += 1;
169            continue;
170        }
171        let bind = info >> 4;
172        if bind != STB_GLOBAL && bind != STB_WEAK {
173            i += 1;
174            continue;
175        }
176        if read_u16(sym.add(6)) == 0 {
177            i += 1;
178            continue;
179        }
180        let name = strtab.wrapping_add(read_u32(sym) as usize);
181        if cstr_eq(name as *const u8, want) {
182            return load_base.wrapping_add(read_u64(sym.add(8)) as usize);
183        }
184        i += 1;
185    }
186    0
187}
188
189/// Try the two names the kernel architectures use for the same entry point.
190unsafe fn vdso_lookup_either(vdso_name: &[u8], kernel_name: &[u8]) -> usize {
191    let p = vdso_lookup(vdso_name);
192    if p != 0 {
193        return p;
194    }
195    vdso_lookup(kernel_name)
196}
197
198#[inline]
199unsafe fn vdso_clock_gettime() -> usize {
200    let p = CLOCK_GETTIME_VDSO.load(Ordering::Relaxed);
201    if p != usize::MAX {
202        return p;
203    }
204    let p = vdso_lookup_either(b"__vdso_clock_gettime", b"__kernel_clock_gettime");
205    CLOCK_GETTIME_VDSO.store(p, Ordering::Relaxed);
206    p
207}
208
209#[no_mangle]
210pub unsafe extern "C" fn clock_gettime(clockid: c_int, ts: *mut timespec) -> c_int {
211    let p = vdso_clock_gettime();
212    if p != 0 {
213        let f: unsafe extern "C" fn(c_int, *mut timespec) -> c_int = core::mem::transmute(p);
214        let r = f(clockid, ts);
215        if r == 0 {
216            return 0;
217        }
218        if r == -EINVAL {
219            ERRNO!() = EINVAL;
220            return -1;
221        }
222        // Unsupported clock or an old vDSO: fall back to the syscall.
223    }
224    let r = sys_clock_gettime(clockid, ts);
225    if r < 0 {
226        ERRNO!() = (-r) as c_int;
227        -1
228    } else {
229        0
230    }
231}
232
233unsafe extern "C" fn getcpu_init(cpu: *mut u32, node: *mut u32, unused: *mut c_void) -> c_long {
234    let p = vdso_lookup_either(b"__vdso_getcpu", b"__kernel_getcpu");
235    GETCPU_VDSO.store(p as *mut c_void, Ordering::Relaxed);
236    if p == 0 {
237        -(ENOSYS_VAL as c_long)
238    } else {
239        let f: unsafe extern "C" fn(*mut u32, *mut u32, *mut c_void) -> c_long =
240            core::mem::transmute(p);
241        f(cpu, node, unused)
242    }
243}
244
245/// sched_getcpu() fast path.  Returns false when the vDSO is unavailable or
246/// declines this CPU, in which case the caller issues the real syscall.
247#[inline]
248unsafe fn vdso_getcpu(cpu: *mut u32) -> bool {
249    let p = GETCPU_VDSO.load(Ordering::Relaxed) as usize;
250    if p == 0 {
251        return false;
252    }
253    let f: unsafe extern "C" fn(*mut u32, *mut u32, *mut c_void) -> c_long =
254        core::mem::transmute(p);
255    f(cpu, core::ptr::null_mut(), core::ptr::null_mut()) == 0
256}
257
258// ============================================================
259// vDSO getrandom
260// ============================================================
261
262#[inline]
263unsafe fn tcb_slot(offset: usize) -> *mut usize {
264    let tp = arch_get_tp_fast();
265    if tp == 0 {
266        return core::ptr::null_mut();
267    }
268    #[cfg(target_arch = "x86_64")]
269    let tcb = tp;
270    #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
271    let tcb = tp - 256;
272    if core::ptr::read_unaligned(tcb as *const usize) != tcb {
273        return core::ptr::null_mut();
274    }
275    (tcb + offset) as *mut usize
276}
277
278#[inline]
279unsafe fn getrandom_slot() -> *mut usize {
280    tcb_slot(GETRANDOM_TCB_OFFSET)
281}
282
283/// Query the vDSO once.  Returns true when getrandom() may use the vDSO and
284/// the per-thread state layout has been recorded.
285unsafe fn vgetrandom_init() -> bool {
286    match VGETRANDOM_STATE.load(Ordering::Acquire) {
287        VGETRANDOM_READY => return true,
288        VGETRANDOM_DISABLED => return false,
289        VGETRANDOM_UNINIT => {}
290        // Another thread is querying right now; use the syscall rather than
291        // waiting, so getrandom() always makes progress.
292        _ => return false,
293    }
294    if VGETRANDOM_STATE
295        .compare_exchange(
296            VGETRANDOM_UNINIT,
297            VGETRANDOM_INITIALIZING,
298            Ordering::AcqRel,
299            Ordering::Acquire,
300        )
301        .is_err()
302    {
303        return false;
304    }
305
306    let p = vdso_lookup_either(b"__vdso_getrandom", b"__kernel_getrandom");
307    let mut ready = false;
308    if p != 0 {
309        let query: unsafe extern "C" fn(
310            *mut u8,
311            usize,
312            u32,
313            *mut VgetrandomParams,
314            usize,
315        ) -> isize = core::mem::transmute(p);
316        let mut params: VgetrandomParams = core::mem::zeroed();
317        if query(core::ptr::null_mut(), 0, 0, &mut params, usize::MAX) == 0 {
318            let page = crate::getauxval(AT_PAGESZ) as usize;
319            let page = if page == 0 { 4096 } else { page };
320            let size = params.size_of_opaque_state as usize;
321            if size > 0 && size <= page {
322                VGETRANDOM_FN.store(p, Ordering::Relaxed);
323                VGETRANDOM_SIZE.store(size, Ordering::Relaxed);
324                VGETRANDOM_PROT.store(params.mmap_prot as usize, Ordering::Relaxed);
325                VGETRANDOM_FLAGS.store(params.mmap_flags as usize, Ordering::Relaxed);
326                VGETRANDOM_PAGE.store(page, Ordering::Relaxed);
327                ready = true;
328            }
329        }
330    }
331    VGETRANDOM_STATE.store(
332        if ready {
333            VGETRANDOM_READY
334        } else {
335            VGETRANDOM_DISABLED
336        },
337        Ordering::Release,
338    );
339    ready
340}
341
342/// Get (or allocate) this thread's opaque vDSO state.  Returns null when the
343/// syscall must be used instead.
344unsafe fn vgetrandom_thread_state() -> *mut u8 {
345    let slot = getrandom_slot();
346    if slot.is_null() {
347        return core::ptr::null_mut();
348    }
349    // The TCB is only 4-byte aligned when a loaded DSO's TLS alignment is 4,
350    // so the slot must not be touched through an &AtomicUsize (unaligned
351    // atomic references panic in debug builds and are not atomic on all
352    // architectures).  Only this thread, including its signal handlers, uses
353    // the slot; unaligned loads/stores plus a try-lock are sufficient.
354    let mut state = core::ptr::read_unaligned(slot);
355    if state == 0 {
356        if VGETRANDOM_ALLOC
357            .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
358            .is_err()
359        {
360            return core::ptr::null_mut();
361        }
362        // A previous signal handler may have completed the allocation while
363        // this invocation was interrupted before taking the lock.
364        state = core::ptr::read_unaligned(slot);
365        if state != 0 {
366            VGETRANDOM_ALLOC.store(0, Ordering::Release);
367            return if state == STATE_ALLOCATING {
368                core::ptr::null_mut()
369            } else {
370                state as *mut u8
371            };
372        }
373        core::ptr::write_unaligned(slot, STATE_ALLOCATING);
374        let saved_errno = ERRNO!();
375        let page = VGETRANDOM_PAGE.load(Ordering::Relaxed);
376        let prot = VGETRANDOM_PROT.load(Ordering::Relaxed) as i32;
377        let flags = VGETRANDOM_FLAGS.load(Ordering::Relaxed) as i32;
378        let mapped = sys_mmap(core::ptr::null_mut(), page, prot, flags, -1, 0);
379        if mapped.is_null() || mapped == MMAP_FAILED {
380            ERRNO!() = saved_errno;
381            core::ptr::write_unaligned(slot, 0);
382            VGETRANDOM_ALLOC.store(0, Ordering::Release);
383            return core::ptr::null_mut();
384        }
385        // fork() copies the state page.  Wiping it in the child forces the
386        // kernel's generation check to reseed instead of reusing the parent's
387        // buffered bytes.  Without this guarantee, do not use the vDSO.
388        let advice = <Arch as Syscalls>::syscall3(
389            sysno(28, 233),
390            mapped as i64,
391            page as i64,
392            MADV_WIPEONFORK as i64,
393        );
394        if advice < 0 {
395            ERRNO!() = saved_errno;
396            sys_munmap(mapped, page);
397            core::ptr::write_unaligned(slot, 0);
398            VGETRANDOM_ALLOC.store(0, Ordering::Release);
399            VGETRANDOM_STATE.store(VGETRANDOM_DISABLED, Ordering::Release);
400            return core::ptr::null_mut();
401        }
402        // Keep the state from straddling a page boundary; mmap returns a
403        // page-aligned block and vgetrandom_init() rejected oversized states.
404        core::ptr::write_unaligned(slot, mapped as usize);
405        VGETRANDOM_ALLOC.store(0, Ordering::Release);
406        state = mapped as usize;
407    }
408    if state == STATE_ALLOCATING {
409        return core::ptr::null_mut();
410    }
411    state as *mut u8
412}
413
414/// vDSO getrandom().  Returns None when the caller must issue the syscall.
415unsafe fn vgetrandom(buf: *mut u8, len: usize, flags: u32) -> Option<isize> {
416    if len == 0 || !vgetrandom_init() {
417        return None;
418    }
419    let state = vgetrandom_thread_state();
420    if state.is_null() {
421        return None;
422    }
423    let p = VGETRANDOM_FN.load(Ordering::Relaxed);
424    if p == 0 {
425        return None;
426    }
427    let size = VGETRANDOM_SIZE.load(Ordering::Relaxed);
428    let f: unsafe extern "C" fn(*mut u8, usize, u32, *mut u8, usize) -> isize =
429        core::mem::transmute(p);
430    Some(f(buf, len, flags, state, size))
431}
432
433/// Release a thread's getrandom state when it exits.
434pub unsafe fn vdso_getrandom_thread_exit() {
435    let slot = getrandom_slot();
436    if slot.is_null() {
437        return;
438    }
439    let state = core::ptr::read_unaligned(slot);
440    if state > STATE_ALLOCATING {
441        core::ptr::write_unaligned(slot, 0);
442        let page = VGETRANDOM_PAGE.load(Ordering::Relaxed);
443        sys_munmap(state as *mut u8, page);
444    }
445}