Skip to content

Commit 7db94cd

Browse files
authored
Test and remediate ghsa-4ch3 for release 36 (#13728)
* Test and remediate ghsa-4ch3 for release 36 * add failing tests demonstrating reported behavior * add a test for renaming file across perm domains * remediate hardlink creation and renaming to forbid crossing permission domains * fixup renaming test-program to get away with dumb structure of testing apparatus * rustfmt * fixup wasi-common tests
1 parent 62c551c commit 7db94cd

13 files changed

Lines changed: 490 additions & 22 deletions

RELEASES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 36.0.12
2+
3+
Released 2026-06-24.
4+
5+
### Fixed
6+
7+
* WASI hard links and renames check wasmtime-wasi's FilePerms for destination
8+
[GHSA-4ch3-9j33-3pmj](https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-4ch3-9j33-3pmj)
9+
10+
--------------------------------------------------------------------------------
11+
112
## 36.0.11
213

314
Released 2026-06-15.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#![expect(unsafe_op_in_unsafe_fn, reason = "old code, not worth updating yet")]
2+
3+
use std::env;
4+
use std::process;
5+
use test_programs::preview1::open_scratch_directory;
6+
7+
const RW_ALIAS_FILENAME: &str = "alias.txt";
8+
const RO_TEST_FILENAME: &str = "test.txt";
9+
const RO_EXPECTED_CONTENTS: &[u8] = b"read only test file\n";
10+
11+
unsafe fn test_ro_file_has_expected_contents(dir_fd: wasip1::Fd) {
12+
// Open a file for reading
13+
let file_fd = wasip1::path_open(dir_fd, 0, RO_TEST_FILENAME, 0, wasip1::RIGHTS_FD_READ, 0, 0)
14+
.expect("opening test.txt for reading");
15+
16+
// Read the file's contents
17+
let buffer = &mut [0u8; 100];
18+
let nread = wasip1::fd_read(
19+
file_fd,
20+
&[wasip1::Iovec {
21+
buf: buffer.as_mut_ptr(),
22+
buf_len: buffer.len(),
23+
}],
24+
)
25+
.expect("reading file content");
26+
27+
// The file should be as created by the test harness
28+
assert_eq!(nread, RO_EXPECTED_CONTENTS.len(), "expected untouched file");
29+
assert_eq!(
30+
&buffer[..nread],
31+
RO_EXPECTED_CONTENTS,
32+
"expected untouched file contents"
33+
);
34+
35+
wasip1::fd_close(file_fd).expect("closing the file");
36+
}
37+
38+
unsafe fn test_file_hardlink_across_perms(rw_dir_fd: wasip1::Fd, ro_dir_fd: wasip1::Fd) {
39+
// Check test preconditions.
40+
test_ro_file_has_expected_contents(ro_dir_fd);
41+
42+
// Creating a hard link of the read-only file into a Descriptor under
43+
// which files are read-writable would allow the read-only file to be
44+
// written to. So, this must fail with perm:
45+
let err = wasip1::path_link(ro_dir_fd, 0, RO_TEST_FILENAME, rw_dir_fd, RW_ALIAS_FILENAME);
46+
assert!(
47+
err.is_err(),
48+
"path_link should fail because link source readonly, dest is readwrite"
49+
);
50+
assert_eq!(
51+
err.err().unwrap(),
52+
wasip1::ERRNO_PERM,
53+
"path_link should fail with ERRNO_PERM"
54+
);
55+
56+
// Check that contents of link dest did not change
57+
test_ro_file_has_expected_contents(ro_dir_fd);
58+
}
59+
60+
fn main() {
61+
let mut args = env::args();
62+
let prog = args.next().unwrap();
63+
let arg = if let Some(arg) = args.next() {
64+
arg
65+
} else {
66+
eprintln!("usage: {prog} <scratch directory>");
67+
process::exit(1);
68+
};
69+
70+
// Open read-write scratch directory
71+
let rw_dir_fd = match open_scratch_directory(&arg) {
72+
Ok(dir_fd) => dir_fd,
73+
Err(err) => {
74+
eprintln!("failed to open scratch directory: {err}");
75+
process::exit(1)
76+
}
77+
};
78+
79+
// This test program requires a special preopen at the path "readonly",
80+
// which the host enforces as read-only. Unlike other test programs, this
81+
// directory's path not passed in as an argument, because modifications to
82+
// the testing harness would be too invasive.
83+
let ro_dir_fd = match open_scratch_directory("readonly") {
84+
Ok(dir_fd) => dir_fd,
85+
Err(err) => {
86+
eprintln!("failed to open readonly preopen: {err}");
87+
process::exit(1)
88+
}
89+
};
90+
91+
// Run the tests.
92+
unsafe {
93+
test_file_hardlink_across_perms(rw_dir_fd, ro_dir_fd);
94+
}
95+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#![expect(unsafe_op_in_unsafe_fn, reason = "old code, not worth updating yet")]
2+
3+
use std::env;
4+
use std::process;
5+
use test_programs::preview1::open_scratch_directory;
6+
7+
const RW_ALIAS_FILENAME: &str = "alias.txt";
8+
const RO_TEST_FILENAME: &str = "test.txt";
9+
const RO_EXPECTED_CONTENTS: &[u8] = b"read only test file\n";
10+
11+
unsafe fn test_ro_file_has_expected_contents(dir_fd: wasip1::Fd) {
12+
// Open a file for reading
13+
let file_fd = wasip1::path_open(dir_fd, 0, RO_TEST_FILENAME, 0, wasip1::RIGHTS_FD_READ, 0, 0)
14+
.expect("opening test.txt for reading");
15+
16+
// Read the file's contents
17+
let buffer = &mut [0u8; 100];
18+
let nread = wasip1::fd_read(
19+
file_fd,
20+
&[wasip1::Iovec {
21+
buf: buffer.as_mut_ptr(),
22+
buf_len: buffer.len(),
23+
}],
24+
)
25+
.expect("reading file content");
26+
27+
// The file should be as created by the test harness
28+
assert_eq!(nread, RO_EXPECTED_CONTENTS.len(), "expected untouched file");
29+
assert_eq!(
30+
&buffer[..nread],
31+
RO_EXPECTED_CONTENTS,
32+
"expected untouched file contents"
33+
);
34+
35+
wasip1::fd_close(file_fd).expect("closing the file");
36+
}
37+
38+
unsafe fn test_file_rename_across_perms(rw_dir_fd: wasip1::Fd, ro_dir_fd: wasip1::Fd) {
39+
// Check test preconditions.
40+
test_ro_file_has_expected_contents(ro_dir_fd);
41+
42+
// Create a hardlink inside the file ro dir so there are two files pointing to
43+
// the read-only file.
44+
match wasip1::path_link(ro_dir_fd, 0, RO_TEST_FILENAME, ro_dir_fd, RW_ALIAS_FILENAME) {
45+
// The readonly dir isnt recreated fresh per test mode in the p2
46+
// runner, so just allow this to fail with exists because its very
47+
// tedious to restructure everything to fix this properly
48+
Ok(()) | Err(wasip1::ERRNO_EXIST) => {}
49+
_ => panic!("should be possible to create link inside ro file domain"),
50+
}
51+
52+
// Renaming that file into the file rw dir should fail with permissions
53+
// error, otherwise it would permit opening the ro file as rw
54+
let err = wasip1::path_rename(ro_dir_fd, RW_ALIAS_FILENAME, rw_dir_fd, RW_ALIAS_FILENAME);
55+
assert!(
56+
err.is_err(),
57+
"path_rename should fail because link source readonly, dest is readwrite"
58+
);
59+
assert_eq!(
60+
err.err().unwrap(),
61+
wasip1::ERRNO_PERM,
62+
"path_rename should fail with ERRNO_PERM"
63+
);
64+
65+
// Check that contents of link dest did not change
66+
test_ro_file_has_expected_contents(ro_dir_fd);
67+
}
68+
69+
fn main() {
70+
let mut args = env::args();
71+
let prog = args.next().unwrap();
72+
let arg = if let Some(arg) = args.next() {
73+
arg
74+
} else {
75+
eprintln!("usage: {prog} <scratch directory>");
76+
process::exit(1);
77+
};
78+
79+
// Open read-write scratch directory
80+
let rw_dir_fd = match open_scratch_directory(&arg) {
81+
Ok(dir_fd) => dir_fd,
82+
Err(err) => {
83+
eprintln!("failed to open scratch directory: {err}");
84+
process::exit(1)
85+
}
86+
};
87+
88+
// This test program requires a special preopen at the path "readonly",
89+
// which the host enforces as read-only. Unlike other test programs, this
90+
// directory's path not passed in as an argument, because modifications to
91+
// the testing harness would be too invasive.
92+
let ro_dir_fd = match open_scratch_directory("readonly") {
93+
Ok(dir_fd) => dir_fd,
94+
Err(err) => {
95+
eprintln!("failed to open readonly preopen: {err}");
96+
process::exit(1)
97+
}
98+
};
99+
100+
// Run the tests.
101+
unsafe {
102+
test_file_rename_across_perms(rw_dir_fd, ro_dir_fd);
103+
}
104+
}

crates/test-programs/src/bin/preview1_file_truncation_readonly.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ unsafe fn test_file_has_expected_contents(dir_fd: wasip1::Fd) {
2020
)
2121
.expect("reading file content");
2222

23-
const EXPECTED_CONTENTS: &[u8] = b"truncation test file\n";
23+
const EXPECTED_CONTENTS: &[u8] = b"read only test file\n";
2424
// The file should be as created by the test harness, not truncated.
2525
assert_eq!(nread, EXPECTED_CONTENTS.len(), "expected untouched file");
2626
assert_eq!(
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#![expect(unsafe_op_in_unsafe_fn, reason = "old code, not worth updating yet")]
2+
use test_programs::wasi::filesystem::preopens;
3+
use test_programs::wasi::filesystem::types::{
4+
Descriptor, DescriptorFlags, ErrorCode, OpenFlags, PathFlags,
5+
};
6+
7+
const RW_ALIAS_FILENAME: &str = "alias.txt";
8+
const RO_TEST_FILENAME: &str = "test.txt";
9+
const RO_EXPECTED_CONTENTS: &[u8] = b"read only test file\n";
10+
11+
unsafe fn test_ro_file_has_expected_contents(dir: &Descriptor) {
12+
// Open a file for reading
13+
let file = dir
14+
.open_at(
15+
PathFlags::empty(),
16+
RO_TEST_FILENAME,
17+
OpenFlags::empty(),
18+
DescriptorFlags::READ,
19+
)
20+
.expect("open test.txt for reading");
21+
22+
// Read the file's contents
23+
let stream = file.read_via_stream(0).unwrap();
24+
let read = stream.blocking_read(100).expect("reading test.txt content");
25+
26+
drop(stream);
27+
drop(file);
28+
assert_eq!(
29+
read, RO_EXPECTED_CONTENTS,
30+
"expected untouched file contents"
31+
);
32+
}
33+
34+
unsafe fn test_file_hardlink_across_perms(rw_dir: &Descriptor, ro_dir: &Descriptor) {
35+
// Check test preconditions.
36+
test_ro_file_has_expected_contents(ro_dir);
37+
38+
// Creating a hard link of the read-only file into a Descriptor under
39+
// which files are read-writable would allow the read-only file to be
40+
// written to. So, this must fail with perm:
41+
let err = ro_dir.link_at(
42+
PathFlags::empty(),
43+
RO_TEST_FILENAME,
44+
rw_dir,
45+
RW_ALIAS_FILENAME,
46+
);
47+
assert!(
48+
err.is_err(),
49+
"link_at should fail because link source is readonly, and dest is readwrite"
50+
);
51+
assert_eq!(
52+
err.err().unwrap(),
53+
ErrorCode::NotPermitted,
54+
"link_at should fail with NotPermitted"
55+
);
56+
57+
// Check that contents of link dest did not change
58+
test_ro_file_has_expected_contents(ro_dir);
59+
}
60+
61+
fn main() {
62+
let args = wasip2::cli::environment::get_arguments();
63+
if args.len() != 2 {
64+
panic!("usage: scratch directory argument required");
65+
}
66+
let preopens = preopens::get_directories();
67+
let rw_path = &args[1];
68+
let (rw_dir, _) = preopens
69+
.iter()
70+
.find(|(_, path)| path == rw_path)
71+
.expect("find preopen specified by argument");
72+
73+
// This test program requires a special preopen at the path "readonly",
74+
// which the host enforces as read-only. Unlike other test programs, this
75+
// directory's path not passed in as an argument, because modifications to
76+
// the testing harness would be too invasive.
77+
let (ro_dir, _) = preopens
78+
.iter()
79+
.find(|(_, path)| path == "readonly")
80+
.expect("find preopen named readonly");
81+
82+
// Run the tests.
83+
unsafe {
84+
test_file_hardlink_across_perms(rw_dir, ro_dir);
85+
}
86+
}

0 commit comments

Comments
 (0)