Skip to content

Commit 24b9d26

Browse files
Snektronsylvestre
authored andcommitted
Add randomize_readdir test utility
This is a simple libc shim that randomizes the results of readdir(). For some functionality, cache hashes are dependent on the order that values are returned from fs::read_dir(), which calls lib readdir(). This library can be inserted into the target application's runtime using LD_PRELOAD=target/debug/librandmize_readdir.so, after which the results of readdir() will return in random order. It is only intended to be used while testing sccache.
1 parent 23f4b54 commit 24b9d26

File tree

4 files changed

+321
-0
lines changed

4 files changed

+321
-0
lines changed

Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,4 @@ dist-tests = ["dist-client", "dist-server"]
205205

206206
[workspace]
207207
exclude = ["tests/test-crate"]
208+
members = ["tests/randomize_readdir"]

tests/randomize_readdir/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
edition = "2021"
3+
name = "randomize_readdir"
4+
version = "0.1.0"
5+
6+
[dependencies]
7+
ctor = "0.2"
8+
libc = "0.2.99"
9+
log = "0.4"
10+
once_cell = "1"
11+
rand = "0.8"
12+
simplelog = "0.12"
13+
14+
[lib]
15+
crate-type = ["cdylib"]

tests/randomize_readdir/src/lib.rs

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// Copyright 2024 Mozilla Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! This library implements a shim that randomizes the results of readdir
16+
//! and readdir64 for testing purposes. This is done by overriding the
17+
//! posix calls associated with reading directories; opendir, fdopendir,
18+
//! readdir, readdir64, and closedir.
19+
//!
20+
//! When readdir or readdir64 is first invoked, the shim will read the
21+
//! entire directory into a vector, shuffle it, and store iteration
22+
//! state inside a custom DirentIterator structure. Note that we
23+
//! assume that no new entries will be added to the directory while
24+
//! iterating, to keep things simple. Also keep in mind that calls to
25+
//! any of the directory reading operations can come from different
26+
//! threads, so the library state has to be kept in thread safe types
27+
//! where appropriate.
28+
//!
29+
//! Calls are dispatched to the "real" implementation in libc by using
30+
//! dlopen with RTLD_NEXT. Unfortunately it seems that the usual libraries
31+
//! for this like libloading do not support RTLD_NEXT, so these
32+
//! functions are just invoked using unsafe calls.
33+
//!
34+
//! To use this library, set LD_PRELOAD=path/to/librandomize_readdir.so.
35+
//! You can verify that the output is random by running for example
36+
//! `LD_PRELOAD=path/to/librandomize_readdir.so ls -U`.
37+
//!
38+
//! To test sccache with librandomize_readdir, export LD_PRELOAD in
39+
//! the integration test and then check that two the second invocation
40+
//! hits the cache. If not, something inside sccache relies implicitly
41+
//! on the order that files are returned from the filesystem, which is
42+
//! not defined, which is not ideal.
43+
44+
use ctor::ctor;
45+
use libc::{c_char, c_int, c_void, dirent, dirent64, dlsym, DIR, RTLD_NEXT};
46+
use log::{error, info};
47+
use once_cell::sync::OnceCell;
48+
use rand::seq::SliceRandom;
49+
use rand::thread_rng;
50+
use simplelog::{Config, LevelFilter, WriteLogger};
51+
use std::collections::HashMap;
52+
use std::env;
53+
use std::ffi::CStr;
54+
use std::fs::File;
55+
use std::process;
56+
use std::sync::RwLock;
57+
58+
type Opendir = unsafe extern "C" fn(dirname: *const c_char) -> *mut DIR;
59+
type Fdopendir = unsafe extern "C" fn(fd: c_int) -> *mut DIR;
60+
type Readdir = unsafe extern "C" fn(dirp: *mut DIR) -> *mut dirent;
61+
type Readdir64 = unsafe extern "C" fn(dirp: *mut DIR) -> *mut dirent64;
62+
type Closedir = unsafe extern "C" fn(dirp: *mut DIR) -> c_int;
63+
64+
struct DirentIterator<Dirent> {
65+
entries: Vec<Dirent>,
66+
index: usize,
67+
}
68+
69+
impl<Dirent> Iterator for DirentIterator<Dirent> {
70+
type Item = *mut Dirent;
71+
72+
fn next(&mut self) -> Option<Self::Item> {
73+
if self.index >= self.entries.len() {
74+
return None;
75+
}
76+
77+
let ptr = &mut self.entries[self.index];
78+
self.index += 1;
79+
Some(ptr)
80+
}
81+
}
82+
83+
struct ReaddirState {
84+
iter: Option<DirentIterator<dirent>>,
85+
iter64: Option<DirentIterator<dirent64>>,
86+
}
87+
88+
struct State {
89+
opendir: Opendir,
90+
fdopendir: Fdopendir,
91+
readdir: Readdir,
92+
readdir64: Readdir64,
93+
closedir: Closedir,
94+
95+
dirs: RwLock<HashMap<usize, ReaddirState>>,
96+
}
97+
98+
impl State {
99+
fn new_opendir(&self, dirp: *mut DIR) {
100+
self.dirs.write().expect("lock poisoned").insert(
101+
dirp as usize,
102+
ReaddirState {
103+
iter: None,
104+
iter64: None,
105+
},
106+
);
107+
}
108+
109+
fn wrapped_readdir_inner<Dirent, GetIter, Readdir>(
110+
&self,
111+
dirp: *mut DIR,
112+
get_iter: GetIter,
113+
readdir: Readdir,
114+
) -> *mut Dirent
115+
where
116+
Dirent: Copy,
117+
GetIter: FnOnce(&mut ReaddirState) -> &mut Option<DirentIterator<Dirent>>,
118+
Readdir: Fn() -> *mut Dirent,
119+
{
120+
self.dirs
121+
.write()
122+
.expect("lock poisoned")
123+
.get_mut(&(dirp as usize))
124+
.map(|dirstate| {
125+
let iter = get_iter(dirstate);
126+
if iter.is_none() {
127+
let mut entries = Vec::new();
128+
129+
loop {
130+
let entry = readdir();
131+
if entry.is_null() {
132+
break;
133+
}
134+
135+
entries.push(unsafe { *entry });
136+
}
137+
138+
entries.shuffle(&mut thread_rng());
139+
140+
*iter = Some(DirentIterator { entries, index: 0 })
141+
}
142+
143+
let iter = iter.as_mut().unwrap();
144+
info!(
145+
"{:p}: reading entry {}/{}",
146+
dirp,
147+
iter.index,
148+
iter.entries.len()
149+
);
150+
iter.next()
151+
})
152+
.flatten()
153+
.unwrap_or(std::ptr::null_mut())
154+
}
155+
156+
fn wrapped_readdir(&self, dirp: *mut DIR) -> *mut dirent {
157+
self.wrapped_readdir_inner(
158+
dirp,
159+
|dirstate| &mut dirstate.iter,
160+
|| unsafe { (self.readdir)(dirp) },
161+
)
162+
}
163+
164+
fn wrapped_readdir64(&self, dirp: *mut DIR) -> *mut dirent64 {
165+
self.wrapped_readdir_inner(
166+
dirp,
167+
|dirstate| &mut dirstate.iter64,
168+
|| unsafe { (self.readdir64)(dirp) },
169+
)
170+
}
171+
}
172+
173+
static STATE: OnceCell<State> = OnceCell::new();
174+
175+
fn load_next<Prototype: Copy>(name: &[u8]) -> Prototype {
176+
unsafe {
177+
let name = CStr::from_bytes_with_nul(name).expect("invalid c-string literal");
178+
let sym = dlsym(RTLD_NEXT, name.as_ptr());
179+
if sym.is_null() {
180+
error!("failed to load libc function {:?}", name.to_string_lossy());
181+
panic!("failed to load libc function pointer");
182+
}
183+
184+
*(&sym as *const *mut c_void as *const Prototype)
185+
}
186+
}
187+
188+
#[ctor]
189+
fn init() {
190+
if let Ok(path) = env::var("RANDOMIZE_READDIR_LOG") {
191+
let path = format!("{}.{}", path, process::id());
192+
WriteLogger::init(
193+
LevelFilter::Info,
194+
Config::default(),
195+
File::create(path).expect("failed to create log file"),
196+
)
197+
.expect("failed to initialize logger");
198+
}
199+
200+
// Force loading on module init.
201+
let opendir = load_next::<Opendir>(b"opendir\0");
202+
let fdopendir = load_next::<Fdopendir>(b"fdopendir\0");
203+
let readdir = load_next::<Readdir>(b"readdir\0");
204+
let readdir64 = load_next::<Readdir64>(b"readdir64\0");
205+
let closedir = load_next::<Closedir>(b"closedir\0");
206+
207+
_ = STATE.get_or_init(|| State {
208+
opendir,
209+
fdopendir,
210+
readdir,
211+
readdir64,
212+
closedir,
213+
dirs: RwLock::new(HashMap::new()),
214+
});
215+
}
216+
217+
#[no_mangle]
218+
pub extern "C" fn opendir(dirname: *const c_char) -> *mut DIR {
219+
let state = STATE.wait();
220+
let dirp = unsafe { (state.opendir)(dirname) };
221+
222+
info!(
223+
"{:p}: opening directory '{}'",
224+
dirp,
225+
unsafe { CStr::from_ptr(dirname) }.to_string_lossy()
226+
);
227+
228+
if !dirp.is_null() {
229+
state.new_opendir(dirp);
230+
}
231+
232+
dirp
233+
}
234+
235+
#[no_mangle]
236+
pub extern "C" fn fdopendir(dirfd: c_int) -> *mut DIR {
237+
let state = STATE.wait();
238+
let dirp = unsafe { (state.fdopendir)(dirfd) };
239+
240+
info!("{:p}: opening directory fd {}", dirp, dirfd);
241+
242+
if !dirp.is_null() {
243+
state.new_opendir(dirp);
244+
}
245+
246+
dirp
247+
}
248+
249+
#[no_mangle]
250+
pub extern "C" fn readdir(dirp: *mut DIR) -> *mut dirent {
251+
STATE.wait().wrapped_readdir(dirp)
252+
}
253+
254+
#[no_mangle]
255+
pub extern "C" fn readdir64(dirp: *mut DIR) -> *mut dirent64 {
256+
STATE.wait().wrapped_readdir64(dirp)
257+
}
258+
259+
#[no_mangle]
260+
pub extern "C" fn closedir(dirp: *mut DIR) -> c_int {
261+
info!("{:p}: closing handle", dirp);
262+
263+
let state = STATE.wait();
264+
265+
state
266+
.dirs
267+
.write()
268+
.expect("lock poisoned")
269+
.remove(&(dirp as usize));
270+
271+
unsafe { (state.closedir)(dirp) }
272+
}

0 commit comments

Comments
 (0)