diff --git a/src/internals.rs b/src/internals.rs index 0ba5c2fc..188574f1 100644 --- a/src/internals.rs +++ b/src/internals.rs @@ -1,6 +1,6 @@ -mod c; -mod cast; -mod helpers; +pub(crate) mod c; +pub(crate) mod cast; +pub(crate) mod helpers; use std::ffi::OsString; use std::mem::size_of; @@ -20,14 +20,13 @@ const NT_PREFIX: [u16; 4] = helpers::utf16s(br"\??\"); /// Ref: const VERBATIM_PREFIX: [u16; 4] = helpers::utf16s(br"\\?\"); -const WCHAR_SIZE: u16 = size_of::() as _; +pub(crate) const WCHAR_SIZE: u16 = size_of::() as _; pub fn create(target: &Path, junction: &Path) -> io::Result<()> { const UNICODE_NULL_SIZE: u16 = WCHAR_SIZE; - const MAX_AVAILABLE_PATH_BUFFER: u16 = c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as u16 + const MAX_PATH_BUFFER: u16 = c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as u16 - c::REPARSE_DATA_BUFFER_HEADER_SIZE - - c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE - - 2 * UNICODE_NULL_SIZE; + - c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE; // We're using low-level APIs to create the junction, and these are more picky about paths. // For example, forward slashes cannot be used as a path separator, so we should try to @@ -37,19 +36,29 @@ pub fn create(target: &Path, junction: &Path) -> io::Result<()> { let target = target.strip_prefix(VERBATIM_PREFIX.as_slice()).unwrap_or(&target); fs::create_dir(junction)?; let file = helpers::open_reparse_point(junction, true)?; - let target_len_in_bytes = { - // "\??\" + target + + // SubstituteName = "\??\" + target (NT path) + let substitute_len_in_bytes = { let len = NT_PREFIX.len().saturating_add(target.len()); let min_len = cmp::min(len, u16::MAX as usize) as u16; - // Len without `UNICODE_NULL` at the end - let target_len_in_bytes = min_len.saturating_mul(WCHAR_SIZE); - // Check for buffer overflow. - if target_len_in_bytes > MAX_AVAILABLE_PATH_BUFFER { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "`target` is too long")); - } - target_len_in_bytes + min_len.saturating_mul(WCHAR_SIZE) + }; + + // PrintName = target (Win32 path, without the \??\ prefix) + let print_name_len_in_bytes = { + let min_len = cmp::min(target.len(), u16::MAX as usize) as u16; + min_len.saturating_mul(WCHAR_SIZE) }; + // Check for buffer overflow: both names + their null terminators must fit + let total_path_buffer = substitute_len_in_bytes + .saturating_add(UNICODE_NULL_SIZE) + .saturating_add(print_name_len_in_bytes) + .saturating_add(UNICODE_NULL_SIZE); + if total_path_buffer > MAX_PATH_BUFFER { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "`target` is too long")); + } + // Redefine the above char array into a ReparseDataBuffer we can work with let mut data = BytesAsReparseDataBuffer::new(); let rdb = data.as_mut_ptr(); @@ -58,24 +67,39 @@ pub fn create(target: &Path, junction: &Path) -> io::Result<()> { addr_of_mut!((*rdb).ReparseTag).write(c::IO_REPARSE_TAG_MOUNT_POINT); addr_of_mut!((*rdb).Reserved).write(0); - // We write target at offset 0 of PathBuffer + // SubstituteName starts at offset 0 in PathBuffer addr_of_mut!((*rdb).ReparseBuffer.SubstituteNameOffset).write(0); - addr_of_mut!((*rdb).ReparseBuffer.SubstituteNameLength).write(target_len_in_bytes); + addr_of_mut!((*rdb).ReparseBuffer.SubstituteNameLength).write(substitute_len_in_bytes); - // We do not use PrintName. However let's set its offset correctly right after SubstituteName - addr_of_mut!((*rdb).ReparseBuffer.PrintNameOffset).write(target_len_in_bytes + UNICODE_NULL_SIZE); - addr_of_mut!((*rdb).ReparseBuffer.PrintNameLength).write(0); + // PrintName starts right after SubstituteName + its null terminator + addr_of_mut!((*rdb).ReparseBuffer.PrintNameOffset).write(substitute_len_in_bytes + UNICODE_NULL_SIZE); + addr_of_mut!((*rdb).ReparseBuffer.PrintNameLength).write(print_name_len_in_bytes); let mut path_buffer_ptr: *mut u16 = addr_of_mut!((*rdb).ReparseBuffer.PathBuffer).cast(); - // Safe because we checked `MAX_AVAILABLE_PATH_BUFFER` + + // Write SubstituteName: "\??\" + target copy_nonoverlapping(NT_PREFIX.as_ptr(), path_buffer_ptr, NT_PREFIX.len()); - // TODO: Do we need to write the NULL-terminator byte? - // It looks like libuv does that. path_buffer_ptr = path_buffer_ptr.add(NT_PREFIX.len()); copy_nonoverlapping(target.as_ptr(), path_buffer_ptr, target.len()); + path_buffer_ptr = path_buffer_ptr.add(target.len()); + + // Null terminator after SubstituteName + path_buffer_ptr.write(0); + path_buffer_ptr = path_buffer_ptr.add(1); + + // Write PrintName: target (Win32 path without \??\ prefix) + copy_nonoverlapping(target.as_ptr(), path_buffer_ptr, target.len()); + path_buffer_ptr = path_buffer_ptr.add(target.len()); + + // Null terminator after PrintName + path_buffer_ptr.write(0); // Set the total size of the data buffer - let size = target_len_in_bytes.wrapping_add(c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE + 2 * UNICODE_NULL_SIZE); + let size = c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE + + substitute_len_in_bytes + + UNICODE_NULL_SIZE + + print_name_len_in_bytes + + UNICODE_NULL_SIZE; addr_of_mut!((*rdb).ReparseDataLength).write(size); size.wrapping_add(c::REPARSE_DATA_BUFFER_HEADER_SIZE) }; diff --git a/src/tests.rs b/src/tests.rs index 2cb3792c..d03ff643 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,8 +1,13 @@ +use std::ffi::OsString; use std::fs::{self, File}; use std::io::{self, Write}; +use std::os::windows::ffi::OsStringExt; use std::os::windows::fs::symlink_file; +use std::os::windows::io::AsRawHandle; #[cfg(miri)] -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::path::PathBuf; +use std::slice; #[cfg(not(miri))] use tempfile::TempDir; @@ -265,3 +270,43 @@ fn create_with_verbatim_prefix_paths() { // get_target returns path without verbatim prefix assert_eq!(super::get_target(&junction).unwrap(), target); } + +#[test] +fn create_populates_print_name() { + // Regression test: the junction reparse point must have a non-empty PrintName + // so that Windows Container layer snapshots correctly preserve the junction target. + use super::internals::{c, cast, helpers, WCHAR_SIZE}; + + let tmpdir = tempfile::tempdir().unwrap(); + let target = tmpdir.path().join("target"); + let junction = tmpdir.path().join("junction"); + fs::create_dir_all(&target).unwrap(); + + super::create(&target, &junction).unwrap(); + + // Read back the raw reparse data + let mut data = cast::BytesAsReparseDataBuffer::new(); + { + let file = helpers::open_reparse_point(&junction, false).unwrap(); + helpers::get_reparse_data_point(file.as_raw_handle(), data.as_mut_ptr()).unwrap(); + } + let rdb = unsafe { data.assume_init() }; + + assert_eq!(rdb.ReparseTag, c::IO_REPARSE_TAG_MOUNT_POINT); + + // Read PrintName + let print_offset = (rdb.ReparseBuffer.PrintNameOffset / WCHAR_SIZE) as usize; + let print_len = (rdb.ReparseBuffer.PrintNameLength / WCHAR_SIZE) as usize; + let print_name = unsafe { + let buf = rdb.ReparseBuffer.PathBuffer.as_ptr().add(print_offset); + slice::from_raw_parts(buf, print_len) + }; + + // PrintName must not be empty + assert!(print_len > 0, "PrintName must not be empty"); + + // PrintName should match what get_target returns (the Win32 path without \??\ prefix) + let print_path = PathBuf::from(OsString::from_wide(print_name)); + let target_path = super::get_target(&junction).unwrap(); + assert_eq!(print_path, target_path, "PrintName should match the target path"); +}