Skip to content

Commit ac85bef

Browse files
Sahnvourkubkon
authored andcommitted
handle lack of privilege to create symbolic links on windows
1 parent e355bcc commit ac85bef

File tree

3 files changed

+60
-9
lines changed

3 files changed

+60
-9
lines changed

lib/std/fs/test.zig

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,20 @@ test "Dir.readLink" {
2525

2626
{
2727
// Create symbolic link by path
28-
try tmp.dir.symLink("file.txt", "symlink1", .{});
28+
tmp.dir.symLink("file.txt", "symlink1", .{}) catch |err| switch (err) {
29+
// Symlink requires admin privileges on windows, so this test can legitimately fail.
30+
error.AccessDenied => return error.SkipZigTest,
31+
else => return err,
32+
};
2933
try testReadLink(tmp.dir, "file.txt", "symlink1");
3034
}
3135
{
3236
// Create symbolic link by path
33-
try tmp.dir.symLink("subdir", "symlink2", .{ .is_directory = true });
37+
tmp.dir.symLink("subdir", "symlink2", .{ .is_directory = true }) catch |err| switch (err) {
38+
// Symlink requires admin privileges on windows, so this test can legitimately fail.
39+
error.AccessDenied => return error.SkipZigTest,
40+
else => return err,
41+
};
3442
try testReadLink(tmp.dir, "subdir", "symlink2");
3543
}
3644
}
@@ -66,15 +74,23 @@ test "readLinkAbsolute" {
6674
const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink1" });
6775

6876
// Create symbolic link by path
69-
try fs.symLinkAbsolute(target_path, symlink_path, .{});
77+
fs.symLinkAbsolute(target_path, symlink_path, .{}) catch |err| switch (err) {
78+
// Symlink requires admin privileges on windows, so this test can legitimately fail.
79+
error.AccessDenied => return error.SkipZigTest,
80+
else => return err,
81+
};
7082
try testReadLinkAbsolute(target_path, symlink_path);
7183
}
7284
{
7385
const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" });
7486
const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink2" });
7587

7688
// Create symbolic link by path
77-
try fs.symLinkAbsolute(target_path, symlink_path, .{ .is_directory = true });
89+
fs.symLinkAbsolute(target_path, symlink_path, .{ .is_directory = true }) catch |err| switch (err) {
90+
// Symlink requires admin privileges on windows, so this test can legitimately fail.
91+
error.AccessDenied => return error.SkipZigTest,
92+
else => return err,
93+
};
7894
try testReadLinkAbsolute(target_path, symlink_path);
7995
}
8096
}

lib/std/os/test.zig

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,20 @@ test "symlink with relative paths" {
125125
try cwd.writeFile("file.txt", "nonsense");
126126

127127
if (builtin.os.tag == .windows) {
128-
try os.windows.CreateSymbolicLink(cwd.fd, &[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' }, &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false);
128+
os.windows.CreateSymbolicLink(
129+
cwd.fd,
130+
&[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' },
131+
&[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
132+
false,
133+
) catch |err| switch (err) {
134+
// Symlink requires admin privileges on windows, so this test can legitimately fail.
135+
error.AccessDenied => {
136+
try cwd.deleteFile("file.txt");
137+
try cwd.deleteFile("symlinked");
138+
return error.SkipZigTest;
139+
},
140+
else => return err,
141+
};
129142
} else {
130143
try os.symlink("file.txt", "symlinked");
131144
}
@@ -183,7 +196,16 @@ test "readlinkat" {
183196

184197
// create a symbolic link
185198
if (builtin.os.tag == .windows) {
186-
try os.windows.CreateSymbolicLink(tmp.dir.fd, &[_]u16{ 'l', 'i', 'n', 'k' }, &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false);
199+
os.windows.CreateSymbolicLink(
200+
tmp.dir.fd,
201+
&[_]u16{ 'l', 'i', 'n', 'k' },
202+
&[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
203+
false,
204+
) catch |err| switch (err) {
205+
// Symlink requires admin privileges on windows, so this test can legitimately fail.
206+
error.AccessDenied => return error.SkipZigTest,
207+
else => return err,
208+
};
187209
} else {
188210
try os.symlinkat("file.txt", tmp.dir.fd, "link");
189211
}

lib/std/os/windows.zig

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ pub fn CreateEventExW(attributes: ?*SECURITY_ATTRIBUTES, nameW: [*:0]const u16,
164164
}
165165
}
166166

167-
pub const DeviceIoControlError = error{Unexpected};
167+
pub const DeviceIoControlError = error{ AccessDenied, Unexpected };
168168

169169
/// A Zig wrapper around `NtDeviceIoControlFile` and `NtFsControlFile` syscalls.
170170
/// It implements similar behavior to `DeviceIoControl` and is meant to serve
@@ -216,6 +216,7 @@ pub fn DeviceIoControl(
216216
};
217217
switch (rc) {
218218
.SUCCESS => {},
219+
.PRIVILEGE_NOT_HELD => return error.AccessDenied,
219220
.INVALID_PARAMETER => unreachable,
220221
else => return unexpectedStatus(rc),
221222
}
@@ -593,6 +594,12 @@ pub const CreateSymbolicLinkError = error{
593594
Unexpected,
594595
};
595596

597+
/// Needs either:
598+
/// - `SeCreateSymbolicLinkPrivilege` privilege
599+
/// or
600+
/// - Developper mode on Windows 10
601+
/// otherwise fails with `error.AccessDenied`. In which case `sym_link_path` may still
602+
/// be created on the file system but will lack reparse processing data applied to it.
596603
pub fn CreateSymbolicLink(
597604
dir: ?HANDLE,
598605
sym_link_path: []const u16,
@@ -710,7 +717,10 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
710717
defer CloseHandle(result_handle);
711718

712719
var reparse_buf: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
713-
_ = try DeviceIoControl(result_handle, FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
720+
_ = DeviceIoControl(result_handle, FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]) catch |err| switch (err) {
721+
error.AccessDenied => unreachable,
722+
else => |e| return e,
723+
};
714724

715725
const reparse_struct = @ptrCast(*const REPARSE_DATA_BUFFER, @alignCast(@alignOf(REPARSE_DATA_BUFFER), &reparse_buf[0]));
716726
switch (reparse_struct.ReparseTag) {
@@ -992,7 +1002,10 @@ pub fn GetFinalPathNameByHandle(
9921002
input_struct.DeviceNameLength = @intCast(USHORT, volume_name.FileNameLength);
9931003
@memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, &volume_name.FileName[0]), volume_name.FileNameLength);
9941004

995-
try DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_POINTS, input_buf[0..], output_buf[0..]);
1005+
DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_POINTS, input_buf[0..], output_buf[0..]) catch |err| switch (err) {
1006+
error.AccessDenied => unreachable,
1007+
else => |e| return e,
1008+
};
9961009
const mount_points_struct = @ptrCast(*const MOUNTMGR_MOUNT_POINTS, &output_buf[0]);
9971010

9981011
const mount_points = @ptrCast(

0 commit comments

Comments
 (0)