From 19b219bc8adbfeb1d1a9faa691563a8e3ff390b8 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Thu, 10 Aug 2023 02:27:24 -0700 Subject: [PATCH] Fix windows.CreateSymbolicLink/ReadLink for non-relative paths This fixes a few things: - Previously, CreateSymbolicLink would always create a relative link if a `dir` was provided, but the relative-ness of a link should be determined by the target path, not the null-ness of the `dir`. - Special handling is now done to symlink to 'rooted' paths correctly (they are treated as a relative link, which is different than how the xToPrefixedFileW functions treat them) - ReadLink now correctly supports UNC paths via a new `ntToWin32Namespace` function which intends to be an analog of `RtlNtPathNameToDosPathName` (RtlNtPathNameToDosPathName is not used because it seems to heap allocate as it takes an RTL_UNICODE_STRING_BUFFER) --- lib/std/fs.zig | 13 ++++- lib/std/os/test.zig | 4 +- lib/std/os/windows.zig | 130 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 128 insertions(+), 19 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index f0be483ca8..f01bcb4f22 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1949,7 +1949,13 @@ pub const Dir = struct { return self.symLinkWasi(target_path, sym_link_path, flags); } if (builtin.os.tag == .windows) { - const target_path_w = try os.windows.sliceToPrefixedFileW(self.fd, target_path); + // Target path does not use sliceToPrefixedFileW because certain paths + // are handled differently when creating a symlink than they would be + // when converting to an NT namespaced path. CreateSymbolicLink in + // symLinkW will handle the necessary conversion. + var target_path_w: os.windows.PathSpace = undefined; + target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); + target_path_w.data[target_path_w.len] = 0; const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); } @@ -1987,7 +1993,10 @@ pub const Dir = struct { /// are null-terminated, WTF16 encoded. pub fn symLinkW( self: Dir, - target_path_w: []const u16, + /// WTF-16, does not need to be NT-prefixed. The NT-prefixing + /// of this path is handled by CreateSymbolicLink. + target_path_w: [:0]const u16, + /// WTF-16, must be NT-prefixed or relative sym_link_path_w: []const u16, flags: SymLinkFlags, ) !void { diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index d5451f64ac..b9ea4f2e2d 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -193,7 +193,7 @@ test "symlink with relative paths" { os.windows.CreateSymbolicLink( cwd.fd, &[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' }, - &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, + &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false, ) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. @@ -351,7 +351,7 @@ test "readlinkat" { os.windows.CreateSymbolicLink( tmp.dir.fd, &[_]u16{ 'l', 'i', 'n', 'k' }, - &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, + &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false, ) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 3340087556..4513d38480 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{ NameTooLong, NoDevice, NetworkNotFound, + BadPathName, Unexpected, }; @@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{ pub fn CreateSymbolicLink( dir: ?HANDLE, sym_link_path: []const u16, - target_path: []const u16, + target_path: [:0]const u16, is_directory: bool, ) CreateSymbolicLinkError!void { const SYMLINK_DATA = extern struct { @@ -745,25 +746,58 @@ pub fn CreateSymbolicLink( }; defer CloseHandle(symlink_handle); + // Relevant portions of the documentation: + // > Relative links are specified using the following conventions: + // > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32". + // > - Current working directory–relative—for example, if the current working directory is + // > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt". + // > Note: If you specify a current working directory–relative link, it is created as an absolute + // > link, due to the way the current working directory is processed based on the user and the thread. + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw + var is_target_absolute = false; + const final_target_path = target_path: { + switch (getNamespacePrefix(u16, target_path)) { + .none => switch (getUnprefixedPathType(u16, target_path)) { + // Rooted paths need to avoid getting put through wToPrefixedFileW + // (and they are treated as relative in this context) + // Note: It seems that rooted paths in symbolic links are relative to + // the drive that the symbolic exists on, not to the CWD's drive. + // So, if the symlink is on C:\ and the CWD is on D:\, + // it will still resolve the path relative to the root of + // the C:\ drive. + .rooted => break :target_path target_path, + else => {}, + }, + // Already an NT path, no need to do anything to it + .nt => break :target_path target_path, + else => {}, + } + var prefixed_target_path = try wToPrefixedFileW(dir, target_path); + // We do this after prefixing to ensure that drive-relative paths are treated as absolute + is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span()); + break :target_path prefixed_target_path.span(); + }; + // prepare reparse data buffer var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; - const buf_len = @sizeOf(SYMLINK_DATA) + target_path.len * 4; + const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4; const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2; + const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path); const symlink_data = SYMLINK_DATA{ .ReparseTag = IO_REPARSE_TAG_SYMLINK, .ReparseDataLength = @as(u16, @intCast(buf_len - header_len)), .Reserved = 0, - .SubstituteNameOffset = @as(u16, @intCast(target_path.len * 2)), - .SubstituteNameLength = @as(u16, @intCast(target_path.len * 2)), + .SubstituteNameOffset = @as(u16, @intCast(final_target_path.len * 2)), + .SubstituteNameLength = @as(u16, @intCast(final_target_path.len * 2)), .PrintNameOffset = 0, - .PrintNameLength = @as(u16, @intCast(target_path.len * 2)), - .Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0, + .PrintNameLength = @as(u16, @intCast(final_target_path.len * 2)), + .Flags = if (!target_is_absolute) SYMLINK_FLAG_RELATIVE else 0, }; @memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data)); - @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path))); - const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2; - @memcpy(buffer[paths_start..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path))); + @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path))); + const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2; + @memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path))); _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null); } @@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin } fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 { - const prefix = [_]u16{ '\\', '?', '?', '\\' }; - var start_index: usize = 0; - if (!is_relative and std.mem.startsWith(u16, path, &prefix)) { - start_index = prefix.len; - } - const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable; + const win32_namespace_path = path: { + if (is_relative) break :path path; + const win32_path = ntToWin32Namespace(path) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.NotNtPath => break :path path, + }; + break :path win32_path.span(); + }; + const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable; return out_buffer[0..out_len]; } @@ -2393,6 +2430,69 @@ test getUnprefixedPathType { try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c")); } +/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation. +/// The possible transformations are: +/// \??\C:\Some\Path -> C:\Some\Path +/// \??\UNC\server\share\foo -> \\server\share\foo +/// If the path does not have the NT namespace prefix, then `error.NotNtPath` is returned. +/// +/// Functionality is based on the ReactOS test cases found here: +/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c +pub fn ntToWin32Namespace(path: []const u16) !PathSpace { + if (path.len > PATH_MAX_WIDE) return error.NameTooLong; + + var path_space: PathSpace = undefined; + const namespace_prefix = getNamespacePrefix(u16, path); + switch (namespace_prefix) { + .nt => { + var dest_index: usize = 0; + var after_prefix = path[4..]; // after the `\??\` + // The prefix \??\UNC\ means this is a UNC path, in which case the + // `\??\UNC\` should be replaced by `\\` (two backslashes) + // TODO: the "UNC" should technically be matched case-insensitively, but + // it's unlikely to matter since most/all paths passed into this + // function will have come from the OS meaning it should have + // the 'canonical' uppercase UNC. + const is_unc = after_prefix.len >= 4 and std.mem.eql(u16, after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and std.fs.path.PathType.windows.isSep(u16, after_prefix[3]); + if (is_unc) { + path_space.data[0] = '\\'; + dest_index += 1; + // We want to include the last `\` of `\??\UNC\` + after_prefix = path[7..]; + } + @memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix); + path_space.len = dest_index + after_prefix.len; + path_space.data[path_space.len] = 0; + return path_space; + }, + else => return error.NotNtPath, + } +} + +test "ntToWin32Namespace" { + const L = std.unicode.utf8ToUtf16LeStringLiteral; + + try testNtToWin32Namespace(L("UNC"), L("\\??\\UNC")); + try testNtToWin32Namespace(L("\\\\"), L("\\??\\UNC\\")); + try testNtToWin32Namespace(L("\\\\path1"), L("\\??\\UNC\\path1")); + try testNtToWin32Namespace(L("\\\\path1\\path2"), L("\\??\\UNC\\path1\\path2")); + + try testNtToWin32Namespace(L(""), L("\\??\\")); + try testNtToWin32Namespace(L("C:"), L("\\??\\C:")); + try testNtToWin32Namespace(L("C:\\"), L("\\??\\C:\\")); + try testNtToWin32Namespace(L("C:\\test"), L("\\??\\C:\\test")); + try testNtToWin32Namespace(L("C:\\test\\"), L("\\??\\C:\\test\\")); + + try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("foo"))); + try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("C:\\test"))); + try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("\\\\.\\test"))); +} + +fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void { + const converted = try ntToWin32Namespace(path); + try std.testing.expectEqualSlices(u16, expected, converted.span()); +} + fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize { const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null); if (result == 0) {