Skip to content

Commit 5567931

Browse files
committed
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 fc82ea7 commit 5567931

File tree

4 files changed

+244
-0
lines changed

4 files changed

+244
-0
lines changed

Cargo.lock

Lines changed: 20 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
@@ -206,3 +206,4 @@ dist-tests = ["dist-client", "dist-server"]
206206

207207
[workspace]
208208
exclude = ["tests/test-crate"]
209+
members = ["tests/randomize_readdir"]

tests/randomize_readdir/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
once_cell = "1"
10+
rand = "0.8"
11+
12+
[lib]
13+
crate-type = ["cdylib"]

tests/randomize_readdir/src/lib.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+
use ctor::ctor;
16+
use libc::{c_char, c_int, c_void, dirent, dirent64, dlsym, DIR, RTLD_NEXT};
17+
use once_cell::sync::OnceCell;
18+
use rand::seq::SliceRandom;
19+
use rand::thread_rng;
20+
use std::collections::HashMap;
21+
use std::ffi::CStr;
22+
use std::sync::RwLock;
23+
24+
type Opendir = unsafe extern "C" fn(dirname: *const c_char) -> *mut DIR;
25+
type Fdopendir = unsafe extern "C" fn(fd: c_int) -> *mut DIR;
26+
type Readdir = unsafe extern "C" fn(dirp: *mut DIR) -> *mut dirent;
27+
type Readdir64 = unsafe extern "C" fn(dirp: *mut DIR) -> *mut dirent64;
28+
type Closedir = unsafe extern "C" fn(dirp: *mut DIR) -> c_int;
29+
30+
struct DirentIterator<Dirent> {
31+
entries: Vec<Dirent>,
32+
index: usize,
33+
}
34+
35+
impl<Dirent> Iterator for DirentIterator<Dirent> {
36+
type Item = *mut Dirent;
37+
38+
fn next(&mut self) -> Option<Self::Item> {
39+
if self.index >= self.entries.len() {
40+
return None;
41+
}
42+
43+
let ptr = &mut self.entries[self.index];
44+
self.index += 1;
45+
Some(ptr)
46+
}
47+
}
48+
49+
struct ReaddirState {
50+
iter: Option<DirentIterator<dirent>>,
51+
iter64: Option<DirentIterator<dirent64>>,
52+
}
53+
54+
struct State {
55+
opendir: Opendir,
56+
fdopendir: Fdopendir,
57+
readdir: Readdir,
58+
readdir64: Readdir64,
59+
closedir: Closedir,
60+
61+
dirs: RwLock<HashMap<usize, ReaddirState>>,
62+
}
63+
64+
impl State {
65+
fn new_opendir(&self, dirp: *mut DIR) {
66+
self.dirs.write().expect("lock poisoned").insert(
67+
dirp as usize,
68+
ReaddirState {
69+
iter: None,
70+
iter64: None,
71+
},
72+
);
73+
}
74+
75+
fn wrapped_readdir_inner<Dirent, GetIter, Readdir>(
76+
&self,
77+
dirp: *mut DIR,
78+
get_iter: GetIter,
79+
readdir: Readdir,
80+
) -> *mut Dirent
81+
where
82+
Dirent: Copy,
83+
GetIter: FnOnce(&mut ReaddirState) -> &mut Option<DirentIterator<Dirent>>,
84+
Readdir: Fn() -> *mut Dirent,
85+
{
86+
self.dirs
87+
.write()
88+
.expect("lock poisoned")
89+
.get_mut(&(dirp as usize))
90+
.map(|dirstate| {
91+
let iter = get_iter(dirstate);
92+
if iter.is_none() {
93+
let mut entries = Vec::new();
94+
95+
loop {
96+
let entry = readdir();
97+
if entry.is_null() {
98+
break;
99+
}
100+
101+
entries.push(unsafe { *entry });
102+
}
103+
104+
entries.shuffle(&mut thread_rng());
105+
106+
*iter = Some(DirentIterator { entries, index: 0 })
107+
}
108+
109+
iter.as_mut().unwrap().next()
110+
})
111+
.flatten()
112+
.unwrap_or(std::ptr::null_mut())
113+
}
114+
115+
fn wrapped_readdir(&self, dirp: *mut DIR) -> *mut dirent {
116+
self.wrapped_readdir_inner(
117+
dirp,
118+
|dirstate| &mut dirstate.iter,
119+
|| unsafe { (self.readdir)(dirp) },
120+
)
121+
}
122+
123+
fn wrapped_readdir64(&self, dirp: *mut DIR) -> *mut dirent64 {
124+
self.wrapped_readdir_inner(
125+
dirp,
126+
|dirstate| &mut dirstate.iter64,
127+
|| unsafe { (self.readdir64)(dirp) },
128+
)
129+
}
130+
}
131+
132+
static STATE: OnceCell<State> = OnceCell::new();
133+
134+
fn load_next<Prototype: Copy>(sym: &[u8]) -> Prototype {
135+
unsafe {
136+
let sym = CStr::from_bytes_with_nul(sym).expect("invalid c-string literal");
137+
let sym = dlsym(RTLD_NEXT, sym.as_ptr());
138+
if sym.is_null() {
139+
panic!("failed to load libc function pointer");
140+
}
141+
142+
*(&sym as *const *mut c_void as *const Prototype)
143+
}
144+
}
145+
146+
#[ctor]
147+
fn init() {
148+
// Force loading on module init.
149+
let opendir = load_next::<Opendir>(b"opendir\0");
150+
let fdopendir = load_next::<Fdopendir>(b"fdopendir\0");
151+
let readdir = load_next::<Readdir>(b"readdir\0");
152+
let readdir64 = load_next::<Readdir64>(b"readdir64\0");
153+
let closedir = load_next::<Closedir>(b"closedir\0");
154+
155+
_ = STATE.get_or_init(|| State {
156+
opendir,
157+
fdopendir,
158+
readdir,
159+
readdir64,
160+
closedir,
161+
dirs: RwLock::new(HashMap::new()),
162+
});
163+
}
164+
165+
#[no_mangle]
166+
pub extern "C" fn opendir(dirname: *const c_char) -> *mut DIR {
167+
let state = STATE.wait();
168+
169+
let dirp = unsafe { (state.opendir)(dirname) };
170+
if !dirp.is_null() {
171+
state.new_opendir(dirp);
172+
}
173+
174+
dirp
175+
}
176+
177+
#[no_mangle]
178+
pub extern "C" fn fdopendir(dirfd: c_int) -> *mut DIR {
179+
let state = STATE.wait();
180+
181+
let dirp = unsafe { (state.fdopendir)(dirfd) };
182+
if !dirp.is_null() {
183+
state.new_opendir(dirp);
184+
}
185+
186+
dirp
187+
}
188+
189+
#[no_mangle]
190+
pub extern "C" fn readdir(dirp: *mut DIR) -> *mut dirent {
191+
STATE.wait().wrapped_readdir(dirp)
192+
}
193+
194+
#[no_mangle]
195+
pub extern "C" fn readdir64(dirp: *mut DIR) -> *mut dirent64 {
196+
STATE.wait().wrapped_readdir64(dirp)
197+
}
198+
199+
#[no_mangle]
200+
pub extern "C" fn closedir(dirp: *mut DIR) -> c_int {
201+
let state = STATE.wait();
202+
203+
state
204+
.dirs
205+
.write()
206+
.expect("lock poisoned")
207+
.remove(&(dirp as usize));
208+
209+
unsafe { (state.closedir)(dirp) }
210+
}

0 commit comments

Comments
 (0)