Skip to content
Merged
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
74 changes: 49 additions & 25 deletions src/internals.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,14 +20,13 @@ const NT_PREFIX: [u16; 4] = helpers::utf16s(br"\??\");
/// Ref: <https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry>
const VERBATIM_PREFIX: [u16; 4] = helpers::utf16s(br"\\?\");

const WCHAR_SIZE: u16 = size_of::<u16>() as _;
pub(crate) const WCHAR_SIZE: u16 = size_of::<u16>() 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
Expand All @@ -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();
Expand All @@ -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)
};
Expand Down
47 changes: 46 additions & 1 deletion src/tests.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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");
}