Skip to content

Commit ca12e8b

Browse files
wan9chiGPT-5.6codex
committed
test(e2e): reproduce constrained dev shm failure
Co-authored-by: GPT-5.6 <gpt-5.6@openai.com> Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 4e0e749 commit ca12e8b

13 files changed

Lines changed: 191 additions & 7 deletions

File tree

Cargo.lock

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

crates/fspy/src/windows/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ impl SpyImpl {
6666
Ok(Self { ansi_dll_path_with_nul: ansi_dll_path_with_nul.into() })
6767
}
6868

69-
#[expect(clippy::unused_async, reason = "async signature required by SpyImpl trait")]
69+
#[expect(
70+
clippy::unused_async,
71+
clippy::unused_async_trait_impl,
72+
reason = "platform implementations share an async call site"
73+
)]
7074
pub(crate) async fn spawn(
7175
&self,
7276
mut command: Command,

crates/fspy/tests/rust_std.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ mod test_utils;
22

33
use std::{
44
env::current_dir,
5-
fs::{File, OpenOptions},
5+
fs::{self, File, OpenOptions},
66
process::Stdio,
77
};
88

@@ -35,6 +35,22 @@ async fn open_write() -> anyhow::Result<()> {
3535
Ok(())
3636
}
3737

38+
#[test(tokio::test)]
39+
async fn metadata() -> anyhow::Result<()> {
40+
let tmp_dir = tempfile::tempdir()?;
41+
let tmp_path = tmp_dir.path().join("hello");
42+
File::create(&tmp_path)?;
43+
let tmp_path_str = tmp_path.to_str().unwrap().to_owned();
44+
45+
let accesses = track_fn!(tmp_path_str, |tmp_path_str: String| {
46+
let _ = fs::metadata(tmp_path_str);
47+
})
48+
.await?;
49+
assert_contains(&accesses, tmp_path.as_path(), AccessMode::READ);
50+
51+
Ok(())
52+
}
53+
3854
#[test(tokio::test)]
3955
async fn readdir() -> anyhow::Result<()> {
4056
let tmpdir = tempfile::tempdir()?;

crates/fspy_preload_unix/src/interceptions/stat.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,27 @@ unsafe extern "C" fn fstatat(
4141
// SAFETY: calling the original libc fstatat() with the same arguments forwarded from the interposed function
4242
unsafe { fstatat::original()(dirfd, pathname, buf, flags) }
4343
}
44+
45+
#[cfg(target_os = "linux")]
46+
intercept!(statx: unsafe extern "C" fn(
47+
dirfd: c_int,
48+
pathname: *const c_char,
49+
flags: c_int,
50+
mask: libc::c_uint,
51+
statxbuf: *mut libc::statx,
52+
) -> c_int);
53+
#[cfg(target_os = "linux")]
54+
unsafe extern "C" fn statx(
55+
dirfd: c_int,
56+
pathname: *const c_char,
57+
flags: c_int,
58+
mask: libc::c_uint,
59+
statxbuf: *mut libc::statx,
60+
) -> c_int {
61+
// SAFETY: dirfd and pathname are valid arguments provided by the caller of the interposed function.
62+
unsafe {
63+
handle_open(PathAt(dirfd, pathname), AccessMode::READ);
64+
}
65+
// SAFETY: calling the original libc statx() with the same arguments forwarded from the interposed function.
66+
unsafe { statx::original()(dirfd, pathname, flags, mask, statxbuf) }
67+
}

crates/vite_powershell/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ use vite_path::{AbsolutePath, AbsolutePathBuf};
2727
pub const POWERSHELL_PREFIX: &[&str] =
2828
&["-NoProfile", "-NoLogo", "-ExecutionPolicy", "Bypass", "-File"];
2929

30-
/// Cached location of the `PowerShell` host. Prefers cross-platform
31-
/// `pwsh.exe` when present, falling back to the Windows built-in
32-
/// `powershell.exe`. Returns `None` on non-Windows or when neither host
33-
/// is on `PATH`.
30+
/// Cached location of the `PowerShell` host.
31+
///
32+
/// Prefers cross-platform `pwsh.exe` when present, falling back to the Windows
33+
/// built-in `powershell.exe`. Returns `None` on non-Windows or when neither
34+
/// host is on `PATH`.
3435
///
3536
/// Cached as `Arc<AbsolutePath>` so callers that want shared ownership
3637
/// (e.g. `vite_task_plan`'s plan-time rewrite) can do `Arc::clone(host)`

crates/vite_task_bin/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ vite_str = { workspace = true }
3131
vite_task = { workspace = true }
3232
which = { workspace = true }
3333

34+
[target.'cfg(target_os = "linux")'.dependencies]
35+
nix = { workspace = true, features = ["mount", "sched", "user"] }
36+
3437
[dev-dependencies]
3538
cow-utils = { workspace = true }
3639
cp_r = { workspace = true }

crates/vite_task_bin/src/vtt/main.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,16 @@ mod print_file;
2323
mod read_stdin;
2424
mod replace_file_content;
2525
mod rm;
26+
#[cfg(target_os = "linux")]
27+
mod small_dev_shm;
28+
#[cfg(not(target_os = "linux"))]
29+
mod small_dev_shm {
30+
pub fn run(_args: &[String]) -> anyhow::Result<()> {
31+
anyhow::bail!("vtt small_dev_shm is only supported on Linux")
32+
}
33+
}
2634
mod stat_file;
35+
mod stat_long_filename;
2736
mod touch_file;
2837
mod write_file;
2938

@@ -32,7 +41,7 @@ fn main() {
3241
if args.len() < 2 {
3342
eprintln!("Usage: vtt <subcommand> [args...]");
3443
eprintln!(
35-
"Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, stat-file, touch-file, write-file"
44+
"Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, small_dev_shm, stat-file, stat_long_filename, touch-file, write-file"
3645
);
3746
std::process::exit(1);
3847
}
@@ -64,10 +73,12 @@ fn main() {
6473
"read-stdin" => read_stdin::run(),
6574
"replace-file-content" => replace_file_content::run(&args[2..]),
6675
"rm" => rm::run(&args[2..]),
76+
"small_dev_shm" => small_dev_shm::run(&args[2..]).map_err(Into::into),
6777
"stat-file" => {
6878
stat_file::run(&args[2..]);
6979
Ok(())
7080
}
81+
"stat_long_filename" => stat_long_filename::run(&args[2..]),
7182
"touch-file" => touch_file::run(&args[2..]),
7283
"write-file" => write_file::run(&args[2..]),
7384
other => {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#![cfg(target_os = "linux")]
2+
3+
use std::{os::unix::process::ExitStatusExt as _, process::Command};
4+
5+
use anyhow::{Context as _, Result};
6+
use nix::{
7+
mount::{MsFlags, mount},
8+
sched::{CloneFlags, unshare},
9+
unistd::{Gid, Uid},
10+
};
11+
12+
const USAGE: &str = "Usage: vtt small_dev_shm <command> [args...]";
13+
14+
pub fn run(args: &[String]) -> Result<()> {
15+
let (program, command_args) = parse_command(args)?;
16+
run_platform(program, command_args)
17+
}
18+
19+
fn parse_command(args: &[String]) -> Result<(&str, &[String])> {
20+
args.split_first().map(|(program, args)| (program.as_str(), args)).context(USAGE)
21+
}
22+
23+
fn run_platform(program: &str, command_args: &[String]) -> Result<()> {
24+
let uid = Uid::current().as_raw();
25+
let gid = Gid::current().as_raw();
26+
27+
unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS)
28+
.context("unshare user and mount namespaces")?;
29+
30+
std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))
31+
.context("write /proc/self/uid_map")?;
32+
match std::fs::write("/proc/self/setgroups", "deny") {
33+
Ok(()) => {}
34+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
35+
Err(error) => return Err(error).context("write /proc/self/setgroups"),
36+
}
37+
std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))
38+
.context("write /proc/self/gid_map")?;
39+
40+
mount(None::<&str>, "/", None::<&str>, MsFlags::MS_REC | MsFlags::MS_PRIVATE, None::<&str>)
41+
.context("make / recursively private")?;
42+
43+
mount(
44+
Some("tmpfs"),
45+
"/dev/shm",
46+
Some("tmpfs"),
47+
MsFlags::empty(),
48+
Some("nr_blocks=1,huge=never"),
49+
)
50+
.context("mount one-page tmpfs at /dev/shm")?;
51+
52+
let status = Command::new(program)
53+
.args(command_args)
54+
.status()
55+
.context("run command with constrained /dev/shm")?;
56+
let code = status.code().unwrap_or_else(|| status.signal().map_or(1, |signal| 128 + signal));
57+
std::process::exit(code);
58+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::{error::Error, io};
2+
3+
const USAGE: &str = "Usage: vtt stat_long_filename <count>";
4+
5+
pub fn run(args: &[String]) -> Result<(), Box<dyn Error>> {
6+
let count = parse_count(args)?;
7+
access_generated_path(count, metadata)?;
8+
Ok(())
9+
}
10+
11+
fn parse_count(args: &[String]) -> Result<usize, String> {
12+
let [count] = args else { return Err(USAGE.to_owned()) };
13+
count.parse().map_err(|_| USAGE.to_owned())
14+
}
15+
16+
fn generated_path(count: usize) -> String {
17+
"x".repeat(count)
18+
}
19+
20+
fn access_generated_path(
21+
count: usize,
22+
mut metadata: impl FnMut(&str) -> io::Result<()>,
23+
) -> io::Result<()> {
24+
let path = generated_path(count);
25+
match metadata(&path) {
26+
Ok(()) => Ok(()),
27+
Err(error)
28+
if error.kind() == io::ErrorKind::NotFound
29+
|| error.raw_os_error() == Some(libc::ENAMETOOLONG) =>
30+
{
31+
Ok(())
32+
}
33+
Err(error) => Err(error),
34+
}
35+
}
36+
37+
fn metadata(path: &str) -> io::Result<()> {
38+
std::fs::metadata(path).map(|_| ())
39+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)