Skip to content

Commit bce02fc

Browse files
authored
Merge pull request #33 from zanieb/zb/print-name
Populate `PrintName` in junction reparse point
2 parents 2faf120 + 951392f commit bce02fc

File tree

2 files changed

+95
-26
lines changed

2 files changed

+95
-26
lines changed

src/internals.rs

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
mod c;
2-
mod cast;
3-
mod helpers;
1+
pub(crate) mod c;
2+
pub(crate) mod cast;
3+
pub(crate) mod helpers;
44

55
use std::ffi::OsString;
66
use std::mem::size_of;
@@ -20,14 +20,13 @@ const NT_PREFIX: [u16; 4] = helpers::utf16s(br"\??\");
2020
/// Ref: <https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry>
2121
const VERBATIM_PREFIX: [u16; 4] = helpers::utf16s(br"\\?\");
2222

23-
const WCHAR_SIZE: u16 = size_of::<u16>() as _;
23+
pub(crate) const WCHAR_SIZE: u16 = size_of::<u16>() as _;
2424

2525
pub fn create(target: &Path, junction: &Path) -> io::Result<()> {
2626
const UNICODE_NULL_SIZE: u16 = WCHAR_SIZE;
27-
const MAX_AVAILABLE_PATH_BUFFER: u16 = c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as u16
27+
const MAX_PATH_BUFFER: u16 = c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as u16
2828
- c::REPARSE_DATA_BUFFER_HEADER_SIZE
29-
- c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE
30-
- 2 * UNICODE_NULL_SIZE;
29+
- c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE;
3130

3231
// We're using low-level APIs to create the junction, and these are more picky about paths.
3332
// 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<()> {
3736
let target = target.strip_prefix(VERBATIM_PREFIX.as_slice()).unwrap_or(&target);
3837
fs::create_dir(junction)?;
3938
let file = helpers::open_reparse_point(junction, true)?;
40-
let target_len_in_bytes = {
41-
// "\??\" + target
39+
40+
// SubstituteName = "\??\" + target (NT path)
41+
let substitute_len_in_bytes = {
4242
let len = NT_PREFIX.len().saturating_add(target.len());
4343
let min_len = cmp::min(len, u16::MAX as usize) as u16;
44-
// Len without `UNICODE_NULL` at the end
45-
let target_len_in_bytes = min_len.saturating_mul(WCHAR_SIZE);
46-
// Check for buffer overflow.
47-
if target_len_in_bytes > MAX_AVAILABLE_PATH_BUFFER {
48-
return Err(io::Error::new(io::ErrorKind::InvalidInput, "`target` is too long"));
49-
}
50-
target_len_in_bytes
44+
min_len.saturating_mul(WCHAR_SIZE)
45+
};
46+
47+
// PrintName = target (Win32 path, without the \??\ prefix)
48+
let print_name_len_in_bytes = {
49+
let min_len = cmp::min(target.len(), u16::MAX as usize) as u16;
50+
min_len.saturating_mul(WCHAR_SIZE)
5151
};
5252

53+
// Check for buffer overflow: both names + their null terminators must fit
54+
let total_path_buffer = substitute_len_in_bytes
55+
.saturating_add(UNICODE_NULL_SIZE)
56+
.saturating_add(print_name_len_in_bytes)
57+
.saturating_add(UNICODE_NULL_SIZE);
58+
if total_path_buffer > MAX_PATH_BUFFER {
59+
return Err(io::Error::new(io::ErrorKind::InvalidInput, "`target` is too long"));
60+
}
61+
5362
// Redefine the above char array into a ReparseDataBuffer we can work with
5463
let mut data = BytesAsReparseDataBuffer::new();
5564
let rdb = data.as_mut_ptr();
@@ -58,24 +67,39 @@ pub fn create(target: &Path, junction: &Path) -> io::Result<()> {
5867
addr_of_mut!((*rdb).ReparseTag).write(c::IO_REPARSE_TAG_MOUNT_POINT);
5968
addr_of_mut!((*rdb).Reserved).write(0);
6069

61-
// We write target at offset 0 of PathBuffer
70+
// SubstituteName starts at offset 0 in PathBuffer
6271
addr_of_mut!((*rdb).ReparseBuffer.SubstituteNameOffset).write(0);
63-
addr_of_mut!((*rdb).ReparseBuffer.SubstituteNameLength).write(target_len_in_bytes);
72+
addr_of_mut!((*rdb).ReparseBuffer.SubstituteNameLength).write(substitute_len_in_bytes);
6473

65-
// We do not use PrintName. However let's set its offset correctly right after SubstituteName
66-
addr_of_mut!((*rdb).ReparseBuffer.PrintNameOffset).write(target_len_in_bytes + UNICODE_NULL_SIZE);
67-
addr_of_mut!((*rdb).ReparseBuffer.PrintNameLength).write(0);
74+
// PrintName starts right after SubstituteName + its null terminator
75+
addr_of_mut!((*rdb).ReparseBuffer.PrintNameOffset).write(substitute_len_in_bytes + UNICODE_NULL_SIZE);
76+
addr_of_mut!((*rdb).ReparseBuffer.PrintNameLength).write(print_name_len_in_bytes);
6877

6978
let mut path_buffer_ptr: *mut u16 = addr_of_mut!((*rdb).ReparseBuffer.PathBuffer).cast();
70-
// Safe because we checked `MAX_AVAILABLE_PATH_BUFFER`
79+
80+
// Write SubstituteName: "\??\" + target
7181
copy_nonoverlapping(NT_PREFIX.as_ptr(), path_buffer_ptr, NT_PREFIX.len());
72-
// TODO: Do we need to write the NULL-terminator byte?
73-
// It looks like libuv does that.
7482
path_buffer_ptr = path_buffer_ptr.add(NT_PREFIX.len());
7583
copy_nonoverlapping(target.as_ptr(), path_buffer_ptr, target.len());
84+
path_buffer_ptr = path_buffer_ptr.add(target.len());
85+
86+
// Null terminator after SubstituteName
87+
path_buffer_ptr.write(0);
88+
path_buffer_ptr = path_buffer_ptr.add(1);
89+
90+
// Write PrintName: target (Win32 path without \??\ prefix)
91+
copy_nonoverlapping(target.as_ptr(), path_buffer_ptr, target.len());
92+
path_buffer_ptr = path_buffer_ptr.add(target.len());
93+
94+
// Null terminator after PrintName
95+
path_buffer_ptr.write(0);
7696

7797
// Set the total size of the data buffer
78-
let size = target_len_in_bytes.wrapping_add(c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE + 2 * UNICODE_NULL_SIZE);
98+
let size = c::MOUNT_POINT_REPARSE_BUFFER_HEADER_SIZE
99+
+ substitute_len_in_bytes
100+
+ UNICODE_NULL_SIZE
101+
+ print_name_len_in_bytes
102+
+ UNICODE_NULL_SIZE;
79103
addr_of_mut!((*rdb).ReparseDataLength).write(size);
80104
size.wrapping_add(c::REPARSE_DATA_BUFFER_HEADER_SIZE)
81105
};

src/tests.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
use std::ffi::OsString;
12
use std::fs::{self, File};
23
use std::io::{self, Write};
4+
use std::os::windows::ffi::OsStringExt;
35
use std::os::windows::fs::symlink_file;
6+
use std::os::windows::io::AsRawHandle;
47
#[cfg(miri)]
5-
use std::path::{Path, PathBuf};
8+
use std::path::Path;
9+
use std::path::PathBuf;
10+
use std::slice;
611

712
#[cfg(not(miri))]
813
use tempfile::TempDir;
@@ -265,3 +270,43 @@ fn create_with_verbatim_prefix_paths() {
265270
// get_target returns path without verbatim prefix
266271
assert_eq!(super::get_target(&junction).unwrap(), target);
267272
}
273+
274+
#[test]
275+
fn create_populates_print_name() {
276+
// Regression test: the junction reparse point must have a non-empty PrintName
277+
// so that Windows Container layer snapshots correctly preserve the junction target.
278+
use super::internals::{c, cast, helpers, WCHAR_SIZE};
279+
280+
let tmpdir = tempfile::tempdir().unwrap();
281+
let target = tmpdir.path().join("target");
282+
let junction = tmpdir.path().join("junction");
283+
fs::create_dir_all(&target).unwrap();
284+
285+
super::create(&target, &junction).unwrap();
286+
287+
// Read back the raw reparse data
288+
let mut data = cast::BytesAsReparseDataBuffer::new();
289+
{
290+
let file = helpers::open_reparse_point(&junction, false).unwrap();
291+
helpers::get_reparse_data_point(file.as_raw_handle(), data.as_mut_ptr()).unwrap();
292+
}
293+
let rdb = unsafe { data.assume_init() };
294+
295+
assert_eq!(rdb.ReparseTag, c::IO_REPARSE_TAG_MOUNT_POINT);
296+
297+
// Read PrintName
298+
let print_offset = (rdb.ReparseBuffer.PrintNameOffset / WCHAR_SIZE) as usize;
299+
let print_len = (rdb.ReparseBuffer.PrintNameLength / WCHAR_SIZE) as usize;
300+
let print_name = unsafe {
301+
let buf = rdb.ReparseBuffer.PathBuffer.as_ptr().add(print_offset);
302+
slice::from_raw_parts(buf, print_len)
303+
};
304+
305+
// PrintName must not be empty
306+
assert!(print_len > 0, "PrintName must not be empty");
307+
308+
// PrintName should match what get_target returns (the Win32 path without \??\ prefix)
309+
let print_path = PathBuf::from(OsString::from_wide(print_name));
310+
let target_path = super::get_target(&junction).unwrap();
311+
assert_eq!(print_path, target_path, "PrintName should match the target path");
312+
}

0 commit comments

Comments
 (0)