|
| 1 | +//! Test-only `LD_PRELOAD` library used by the `preexisting_ld_preload` e2e |
| 2 | +//! fixture. Intercepts `open`/`openat` (and their `64` variants) to exercise |
| 3 | +//! two behaviours fspy must tolerate when appended to a pre-existing |
| 4 | +//! `LD_PRELOAD` list: |
| 5 | +//! |
| 6 | +//! 1. For paths containing the marker `preload_test_short_circuit`, the |
| 7 | +//! call is short-circuited with `ENOENT` *without* forwarding to the |
| 8 | +//! next preloaded library. Because fspy is appended after this library |
| 9 | +//! in the preload list, fspy never observes the call — exactly the |
| 10 | +//! property we want to verify. |
| 11 | +//! 2. For every other path the call is forwarded via |
| 12 | +//! `dlsym(RTLD_NEXT, …)`, so fspy still sees the real accesses and can |
| 13 | +//! track them as cache inputs. |
| 14 | +#![cfg(target_os = "linux")] |
| 15 | +#![feature(c_variadic)] |
| 16 | + |
| 17 | +use std::{ |
| 18 | + ffi::{CStr, c_char, c_int}, |
| 19 | + sync::OnceLock, |
| 20 | +}; |
| 21 | + |
| 22 | +const MARKER: &[u8] = b"preload_test_short_circuit"; |
| 23 | + |
| 24 | +fn should_short_circuit(path: *const c_char) -> bool { |
| 25 | + if path.is_null() { |
| 26 | + return false; |
| 27 | + } |
| 28 | + // SAFETY: callers of `open`/`openat` pass a valid NUL-terminated C string |
| 29 | + // (or NULL, handled above). |
| 30 | + let bytes = unsafe { CStr::from_ptr(path) }.to_bytes(); |
| 31 | + bytes.windows(MARKER.len()).any(|w| w == MARKER) |
| 32 | +} |
| 33 | + |
| 34 | +fn fail_with_enoent() -> c_int { |
| 35 | + // SAFETY: `__errno_location` is async-signal-safe and always returns a |
| 36 | + // valid pointer to the per-thread errno. |
| 37 | + unsafe { *libc::__errno_location() = libc::ENOENT }; |
| 38 | + -1 |
| 39 | +} |
| 40 | + |
| 41 | +const fn has_mode_arg(flags: c_int) -> bool { |
| 42 | + flags & libc::O_CREAT != 0 || flags & libc::O_TMPFILE != 0 |
| 43 | +} |
| 44 | + |
| 45 | +type OpenFn = unsafe extern "C" fn(*const c_char, c_int, ...) -> c_int; |
| 46 | +type OpenatFn = unsafe extern "C" fn(c_int, *const c_char, c_int, ...) -> c_int; |
| 47 | + |
| 48 | +fn load_next_fn<F: Copy>(name: &CStr) -> F { |
| 49 | + // SAFETY: `dlsym` with `RTLD_NEXT` returns either NULL or a valid |
| 50 | + // function pointer for a symbol that must exist in libc. The cast is |
| 51 | + // valid because the caller supplies a `F` whose layout is a function |
| 52 | + // pointer of the corresponding libc signature. |
| 53 | + let ptr = unsafe { libc::dlsym(libc::RTLD_NEXT, name.as_ptr()) }; |
| 54 | + assert!(!ptr.is_null(), "dlsym RTLD_NEXT returned null"); |
| 55 | + // SAFETY: see above. |
| 56 | + unsafe { std::mem::transmute_copy(&ptr) } |
| 57 | +} |
| 58 | + |
| 59 | +fn next_open() -> OpenFn { |
| 60 | + static S: OnceLock<OpenFn> = OnceLock::new(); |
| 61 | + *S.get_or_init(|| load_next_fn(c"open")) |
| 62 | +} |
| 63 | +fn next_open64() -> OpenFn { |
| 64 | + static S: OnceLock<OpenFn> = OnceLock::new(); |
| 65 | + *S.get_or_init(|| load_next_fn(c"open64")) |
| 66 | +} |
| 67 | +fn next_openat() -> OpenatFn { |
| 68 | + static S: OnceLock<OpenatFn> = OnceLock::new(); |
| 69 | + *S.get_or_init(|| load_next_fn(c"openat")) |
| 70 | +} |
| 71 | +fn next_openat64() -> OpenatFn { |
| 72 | + static S: OnceLock<OpenatFn> = OnceLock::new(); |
| 73 | + *S.get_or_init(|| load_next_fn(c"openat64")) |
| 74 | +} |
| 75 | + |
| 76 | +/// # Safety |
| 77 | +/// Interposer over libc `open(2)`; same contract as the real function. Must |
| 78 | +/// only be called by the dynamic loader after installation via `LD_PRELOAD`. |
| 79 | +#[unsafe(no_mangle)] |
| 80 | +pub unsafe extern "C" fn open(path: *const c_char, flags: c_int, mut args: ...) -> c_int { |
| 81 | + if should_short_circuit(path) { |
| 82 | + return fail_with_enoent(); |
| 83 | + } |
| 84 | + if has_mode_arg(flags) { |
| 85 | + // SAFETY: `O_CREAT`/`O_TMPFILE` guarantees a `mode_t` follows per |
| 86 | + // the `open(2)` contract. |
| 87 | + let mode: libc::mode_t = unsafe { args.arg() }; |
| 88 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 89 | + unsafe { next_open()(path, flags, mode) } |
| 90 | + } else { |
| 91 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 92 | + unsafe { next_open()(path, flags) } |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +/// # Safety |
| 97 | +/// Interposer over libc `open64(2)`; same contract as the real function. |
| 98 | +#[unsafe(no_mangle)] |
| 99 | +pub unsafe extern "C" fn open64(path: *const c_char, flags: c_int, mut args: ...) -> c_int { |
| 100 | + if should_short_circuit(path) { |
| 101 | + return fail_with_enoent(); |
| 102 | + } |
| 103 | + if has_mode_arg(flags) { |
| 104 | + // SAFETY: `O_CREAT`/`O_TMPFILE` guarantees a `mode_t` follows per |
| 105 | + // the `open64(2)` contract. |
| 106 | + let mode: libc::mode_t = unsafe { args.arg() }; |
| 107 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 108 | + unsafe { next_open64()(path, flags, mode) } |
| 109 | + } else { |
| 110 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 111 | + unsafe { next_open64()(path, flags) } |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +/// # Safety |
| 116 | +/// Interposer over libc `openat(2)`; same contract as the real function. |
| 117 | +#[unsafe(no_mangle)] |
| 118 | +pub unsafe extern "C" fn openat( |
| 119 | + dirfd: c_int, |
| 120 | + path: *const c_char, |
| 121 | + flags: c_int, |
| 122 | + mut args: ... |
| 123 | +) -> c_int { |
| 124 | + if should_short_circuit(path) { |
| 125 | + return fail_with_enoent(); |
| 126 | + } |
| 127 | + if has_mode_arg(flags) { |
| 128 | + // SAFETY: `O_CREAT`/`O_TMPFILE` guarantees a `mode_t` follows per |
| 129 | + // the `openat(2)` contract. |
| 130 | + let mode: libc::mode_t = unsafe { args.arg() }; |
| 131 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 132 | + unsafe { next_openat()(dirfd, path, flags, mode) } |
| 133 | + } else { |
| 134 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 135 | + unsafe { next_openat()(dirfd, path, flags) } |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +/// # Safety |
| 140 | +/// Interposer over libc `openat64(2)`; same contract as the real function. |
| 141 | +#[unsafe(no_mangle)] |
| 142 | +pub unsafe extern "C" fn openat64( |
| 143 | + dirfd: c_int, |
| 144 | + path: *const c_char, |
| 145 | + flags: c_int, |
| 146 | + mut args: ... |
| 147 | +) -> c_int { |
| 148 | + if should_short_circuit(path) { |
| 149 | + return fail_with_enoent(); |
| 150 | + } |
| 151 | + if has_mode_arg(flags) { |
| 152 | + // SAFETY: `O_CREAT`/`O_TMPFILE` guarantees a `mode_t` follows per |
| 153 | + // the `openat64(2)` contract. |
| 154 | + let mode: libc::mode_t = unsafe { args.arg() }; |
| 155 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 156 | + unsafe { next_openat64()(dirfd, path, flags, mode) } |
| 157 | + } else { |
| 158 | + // SAFETY: forwarding the caller's arguments unchanged. |
| 159 | + unsafe { next_openat64()(dirfd, path, flags) } |
| 160 | + } |
| 161 | +} |
0 commit comments