Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,25 @@ fn create_dir_all_with_junctions() {
assert!(d.exists());
}

#[test]
#[cfg(windows)]
fn junction_point_overlong_path() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see this regression test fail without the change. Do you mind opening a second temporary PR containing just the test so that we can run it through CI? We can close it again when we've confirmed the regression test catches the bug.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed failure in #158201.

// Regression test: an `original` path long enough to exceed the inline
// reparse buffer used to be copied past the end of the stack array. It must
// now be rejected with a clean error instead of overflowing.
let tmpdir = tmpdir();
let link = tmpdir.join("junction");

// The `\\?\` prefix bypasses MAX_PATH normalization so the path is copied
// through verbatim. 20_000 code units lands in the old overflow window: it
// passed the previous `> u16::MAX` byte check yet exceeded the buffer.
let mut original = String::from(r"\\?\C:\");
original.push_str(&"a".repeat(20_000));

let err = junction_point(Path::new(&original), &link).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}

#[test]
fn metadata_access_times() {
let start_time = SystemTime::now();
Expand Down
33 changes: 20 additions & 13 deletions library/std/src/sys/fs/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1676,33 +1676,40 @@ pub fn junction_point(original: &Path, link: &Path) -> io::Result<()> {
SubstituteNameLength: u16,
PrintNameOffset: u16,
PrintNameLength: u16,
PathBuffer: [MaybeUninit<u16>; c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize],
// `MAXIMUM_REPARSE_DATA_BUFFER_SIZE` is a size in bytes, but this is a
// buffer of `u16`s, so it holds half as many elements.
PathBuffer: [MaybeUninit<u16>; c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize / 2],

@ChrisDenton ChrisDenton Jun 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't sound right at all. What makes you say MAXIMUM_REPARSE_DATA_BUFFER_SIZE is in bytes?

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, apparently it is.

The protocol docs make no mention of a maximum size, only that it's a 16 bit unsigned integer (hence my surprise at 16kb being a limit). But the public headers for the kernel API do have MAXIMUM_REPARSE_DATA_BUFFER_SIZE in bytes of 16kb.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be changed to an u8 array as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say no. It's the mount-point WCHAR path, and u16 keeps the copy a clean write_copy_of_slice(&abs_path).

}
let data_len = 12 + (abs_path.len() * 2);
if data_len > u16::MAX as usize {
return Err(io::const_error!(io::ErrorKind::InvalidInput, "`original` path is too long"));
}
let data_len = data_len as u16;
let mut header = MountPointBuffer {
ReparseTag: c::IO_REPARSE_TAG_MOUNT_POINT,
ReparseDataLength: data_len,
ReparseDataLength: 0, // filled in below
Reserved: 0,
SubstituteNameOffset: 0,
SubstituteNameLength: (abs_path.len() * 2) as u16,
PrintNameOffset: ((abs_path.len() + 1) * 2) as u16,
PrintNameOffset: (abs_path.len() * 2) as u16,
PrintNameLength: 0,
PathBuffer: [MaybeUninit::uninit(); c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize],
PathBuffer: [MaybeUninit::uninit(); c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize / 2],
};
// The substitute name has an explicit length, so it needs no null
// terminator. Bounds-check and copy in a single step so an over-long path
// fails cleanly instead of overflowing the buffer.
let Some(ptr) = header.PathBuffer.get_mut(..abs_path.len()) else {
return Err(io::const_error!(io::ErrorKind::InvalidInput, "`original` path is too long"));
};
ptr.write_copy_of_slice(&abs_path);
// Total size of the structure: the fixed header fields followed by the path.
let total_len = offset_of!(MountPointBuffer, PathBuffer) + abs_path.len() * 2;
// `ReparseDataLength` counts only the bytes after the 8-byte common header
// (`ReparseTag`, `ReparseDataLength`, `Reserved`).
header.ReparseDataLength =
(total_len - offset_of!(MountPointBuffer, SubstituteNameOffset)) as u16;
unsafe {
let ptr = header.PathBuffer.as_mut_ptr();
ptr.copy_from(abs_path.as_ptr().cast_uninit(), abs_path.len());

let mut ret = 0;
cvt(c::DeviceIoControl(
d.as_raw_handle(),
c::FSCTL_SET_REPARSE_POINT,
(&raw const header).cast::<c_void>(),
data_len as u32 + 8,
total_len as u32,
ptr::null_mut(),
0,
&mut ret,
Expand Down
Loading