Skip to content

Commit ac080f6

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

9 files changed

Lines changed: 275 additions & 2 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/vite_task_bin/Cargo.toml

Lines changed: 3 additions & 1 deletion
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 }
@@ -64,5 +67,4 @@ name = "e2e_snapshots"
6467
harness = false
6568

6669
[lib]
67-
test = false
6870
doctest = false

crates/vite_task_bin/src/vtt/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ mod print_file;
2323
mod read_stdin;
2424
mod replace_file_content;
2525
mod rm;
26+
mod small_dev_shm;
2627
mod stat_file;
28+
mod stat_long_filename;
2729
mod touch_file;
2830
mod write_file;
2931

@@ -32,7 +34,7 @@ fn main() {
3234
if args.len() < 2 {
3335
eprintln!("Usage: vtt <subcommand> [args...]");
3436
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"
37+
"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"
3638
);
3739
std::process::exit(1);
3840
}
@@ -64,10 +66,12 @@ fn main() {
6466
"read-stdin" => read_stdin::run(),
6567
"replace-file-content" => replace_file_content::run(&args[2..]),
6668
"rm" => rm::run(&args[2..]),
69+
"small_dev_shm" => small_dev_shm::run(&args[2..]),
6770
"stat-file" => {
6871
stat_file::run(&args[2..]);
6972
Ok(())
7073
}
74+
"stat_long_filename" => stat_long_filename::run(&args[2..]),
7175
"touch-file" => touch_file::run(&args[2..]),
7276
"write-file" => write_file::run(&args[2..]),
7377
other => {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use std::error::Error;
2+
3+
const USAGE: &str = "Usage: vtt small_dev_shm <command> [args...]";
4+
5+
pub fn run(args: &[String]) -> Result<(), Box<dyn Error>> {
6+
let (program, command_args) = parse_command(args)?;
7+
run_platform(program, command_args)?;
8+
Ok(())
9+
}
10+
11+
fn parse_command(args: &[String]) -> Result<(&str, &[String]), &'static str> {
12+
args.split_first().map(|(program, args)| (program.as_str(), args)).ok_or(USAGE)
13+
}
14+
15+
#[cfg(target_os = "linux")]
16+
fn write_procfs(path: &str, content: &str) -> std::io::Result<()> {
17+
use std::io::Write as _;
18+
19+
let mut file = std::fs::OpenOptions::new().write(true).open(path)?;
20+
file.write_all(content.as_bytes())
21+
}
22+
23+
#[cfg(target_os = "linux")]
24+
fn run_platform(program: &str, command_args: &[String]) -> std::io::Result<()> {
25+
use std::{os::unix::process::ExitStatusExt as _, process::Command};
26+
27+
use nix::{
28+
mount::{MsFlags, mount},
29+
sched::{CloneFlags, unshare},
30+
unistd::{Gid, Uid},
31+
};
32+
33+
fn operation_error(operation: &str, error: impl std::fmt::Display) -> std::io::Error {
34+
std::io::Error::other(format!("{operation}: {error}"))
35+
}
36+
37+
let uid = Uid::current().as_raw();
38+
let gid = Gid::current().as_raw();
39+
40+
unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS)
41+
.map_err(|error| operation_error("unshare user and mount namespaces", error))?;
42+
43+
write_procfs("/proc/self/uid_map", &format!("0 {uid} 1\n"))
44+
.map_err(|error| operation_error("write /proc/self/uid_map", error))?;
45+
match write_procfs("/proc/self/setgroups", "deny") {
46+
Ok(()) => {}
47+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
48+
Err(error) => return Err(operation_error("write /proc/self/setgroups", error)),
49+
}
50+
write_procfs("/proc/self/gid_map", &format!("0 {gid} 1\n"))
51+
.map_err(|error| operation_error("write /proc/self/gid_map", error))?;
52+
53+
mount(None::<&str>, "/", None::<&str>, MsFlags::MS_REC | MsFlags::MS_PRIVATE, None::<&str>)
54+
.map_err(|error| operation_error("make / recursively private", error))?;
55+
56+
mount(
57+
Some("tmpfs"),
58+
"/dev/shm",
59+
Some("tmpfs"),
60+
MsFlags::empty(),
61+
Some("nr_blocks=1,huge=never"),
62+
)
63+
.map_err(|error| operation_error("mount one-page tmpfs at /dev/shm", error))?;
64+
65+
let status = Command::new(program).args(command_args).status()?;
66+
let code = status.code().unwrap_or_else(|| status.signal().map_or(1, |signal| 128 + signal));
67+
std::process::exit(code);
68+
}
69+
70+
#[cfg(not(target_os = "linux"))]
71+
fn run_platform(_program: &str, _command_args: &[String]) -> std::io::Result<()> {
72+
Err(std::io::Error::new(
73+
std::io::ErrorKind::Unsupported,
74+
"vtt small_dev_shm is only supported on Linux",
75+
))
76+
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use super::*;
81+
82+
#[test]
83+
fn requires_a_command() {
84+
assert_eq!(parse_command(&[]).unwrap_err(), USAGE);
85+
}
86+
87+
#[test]
88+
fn parses_command_and_arguments() {
89+
let args = ["vt".to_owned(), "run".to_owned(), "stress".to_owned()];
90+
let (program, command_args) = parse_command(&args).unwrap();
91+
92+
assert_eq!(program, "vt");
93+
assert_eq!(command_args, ["run", "stress"]);
94+
}
95+
96+
#[cfg(target_os = "linux")]
97+
#[test]
98+
fn procfs_writer_does_not_create_missing_files() {
99+
let tempdir = tempfile::tempdir().unwrap();
100+
let missing = tempdir.path().join("missing");
101+
102+
let error = write_procfs(missing.to_str().unwrap(), "content").unwrap_err();
103+
104+
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
105+
assert!(!missing.exists());
106+
}
107+
108+
#[cfg(not(target_os = "linux"))]
109+
#[test]
110+
fn reports_unsupported_platform() {
111+
let error = run_platform("vt", &[]).unwrap_err();
112+
113+
assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
114+
assert_eq!(error.to_string(), "vtt small_dev_shm is only supported on Linux");
115+
}
116+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::{error::Error, io};
2+
3+
const USAGE: &str = "Usage: vtt stat_long_filename <count>";
4+
#[cfg(target_os = "linux")]
5+
const STATX_BASIC_STATS: u32 = 0x07ff;
6+
7+
pub fn run(args: &[String]) -> Result<(), Box<dyn Error>> {
8+
let count = parse_count(args)?;
9+
access_generated_path(count, metadata)?;
10+
Ok(())
11+
}
12+
13+
fn parse_count(args: &[String]) -> Result<usize, String> {
14+
let [count] = args else { return Err(USAGE.to_owned()) };
15+
count.parse().map_err(|_| USAGE.to_owned())
16+
}
17+
18+
fn generated_path(count: usize) -> String {
19+
"x".repeat(count)
20+
}
21+
22+
fn access_generated_path(
23+
count: usize,
24+
mut metadata: impl FnMut(&str) -> io::Result<()>,
25+
) -> io::Result<()> {
26+
let path = generated_path(count);
27+
match metadata(&path) {
28+
Ok(()) => Ok(()),
29+
Err(error)
30+
if error.kind() == io::ErrorKind::NotFound
31+
|| error.raw_os_error() == Some(libc::ENAMETOOLONG) =>
32+
{
33+
Ok(())
34+
}
35+
Err(error) => Err(error),
36+
}
37+
}
38+
39+
#[cfg(target_os = "linux")]
40+
fn metadata(path: &str) -> io::Result<()> {
41+
use std::{ffi::CString, mem::MaybeUninit};
42+
43+
let path = CString::new(path)
44+
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL"))?;
45+
// Linux's `struct statx` is 256 bytes and 64-bit aligned on supported targets.
46+
let mut statx = MaybeUninit::<[u64; 32]>::uninit();
47+
// SAFETY: `path` is NUL-terminated, `statx` points to writable storage, and
48+
// all six variadic arguments use the type read by fspy's syscall interceptor.
49+
let result = unsafe {
50+
libc::syscall(
51+
libc::SYS_statx,
52+
libc::c_long::from(libc::AT_FDCWD),
53+
path.as_ptr() as libc::c_long,
54+
0 as libc::c_long,
55+
libc::c_long::from(STATX_BASIC_STATS),
56+
statx.as_mut_ptr() as libc::c_long,
57+
0 as libc::c_long,
58+
)
59+
};
60+
if result == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
61+
}
62+
63+
#[cfg(not(target_os = "linux"))]
64+
fn metadata(path: &str) -> io::Result<()> {
65+
std::fs::metadata(path).map(|_| ())
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use std::path::Path;
71+
72+
use super::*;
73+
74+
fn strings(args: &[&str]) -> Vec<String> {
75+
args.iter().map(|arg| (*arg).to_owned()).collect()
76+
}
77+
78+
#[test]
79+
fn parses_exactly_one_usize_count() {
80+
assert_eq!(parse_count(&strings(&["1048576"])).unwrap(), 1_048_576);
81+
assert_eq!(parse_count(&[]).unwrap_err(), USAGE);
82+
assert_eq!(parse_count(&strings(&["1", "2"])).unwrap_err(), USAGE);
83+
assert_eq!(parse_count(&strings(&["many"])).unwrap_err(), USAGE);
84+
}
85+
86+
#[test]
87+
fn generates_an_exact_relative_non_nul_ascii_path() {
88+
let path = generated_path(4096);
89+
90+
assert_eq!(path.len(), 4096);
91+
assert!(Path::new(&path).is_relative());
92+
assert!(path.is_ascii());
93+
assert!(!path.as_bytes().contains(&0));
94+
}
95+
96+
#[test]
97+
fn makes_one_metadata_access_and_accepts_enametoolong() {
98+
let mut calls = 0;
99+
100+
access_generated_path(4096, |path| {
101+
calls += 1;
102+
assert_eq!(path.len(), 4096);
103+
Err(io::Error::from_raw_os_error(libc::ENAMETOOLONG))
104+
})
105+
.unwrap();
106+
107+
assert_eq!(calls, 1);
108+
}
109+
110+
#[test]
111+
fn accepts_a_missing_path() {
112+
access_generated_path(8, |_| Err(io::Error::from(io::ErrorKind::NotFound))).unwrap();
113+
}
114+
115+
#[test]
116+
fn propagates_unexpected_metadata_errors() {
117+
let error =
118+
access_generated_path(8, |_| Err(io::Error::from(io::ErrorKind::PermissionDenied)))
119+
.unwrap_err();
120+
121+
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
122+
}
123+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[[e2e]]
2+
name = "constrained_dev_shm"
3+
comment = """
4+
Mounting a one-page `/dev/shm` reproduces the SIGBUS seen before fspy moved its shared-memory backing to memfd.
5+
"""
6+
platform = "linux-gnu"
7+
steps = [["vtt", "small_dev_shm", "vt", "run", "stress"]]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# constrained_dev_shm
2+
3+
Mounting a one-page `/dev/shm` reproduces the SIGBUS seen before fspy moved its shared-memory backing to memfd.
4+
5+
## `vtt small_dev_shm vt run stress`
6+
7+
**Exit code:** 135
8+
9+
```
10+
$ vtt stat_long_filename 1048576
11+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"tasks": {
3+
"stress": {
4+
"command": "vtt stat_long_filename 1048576",
5+
"cache": true
6+
}
7+
}
8+
}

0 commit comments

Comments
 (0)