authorgravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2020-07-19 22:25:00+02:00
committergravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2020-07-22 08:51:22+02:00
loge0b77a6b77614538d18b9f0b9e3e7e434ccee4ff
tree04b40728d9b672ef160431fe550e389863ce666d
parent3c8ceb674eb00ac0a70d6a0742234ce520eccf06

Ensure Dir.deleteTree does not dereference symlinks

Otherwise, the behaviour can lead to unexpected results, resulting in removing an entire tree that's not necessarily under the root. Furthermore, this change is needed if are to properly handle dir symlinks on Windows. Without explicitly requiring that a directory or file is opened with `FILE_OPEN_REPARSE_POINT`, Windows automatically dereferences all symlinks along the way. This commit adds another option to `OpenDirOptions`, namely `.no_follow`, which defaults to `false` and can be used to specifically open a directory symlink on Windows or call `openat` with `O_NOFOLLOW` flag in POSIX.

4 files changed, 25 insertions(+), 22 deletions(-)

lib/std/fs.zig+18-9
...@@ -951,6 +951,9 @@ pub const Dir = struct {...@@ -951,6 +951,9 @@ pub const Dir = struct {
951 /// `true` means the opened directory can be scanned for the files and sub-directories951 /// `true` means the opened directory can be scanned for the files and sub-directories
952 /// of the result. It means the `iterate` function can be called.952 /// of the result. It means the `iterate` function can be called.
953 iterate: bool = false,953 iterate: bool = false,
954
955 /// `true` means it won't dereference the symlink.
956 no_follow: bool = false,
954 };957 };
955958
956 /// Opens a directory at the given path. The directory is a system resource that remains959 /// Opens a directory at the given path. The directory is a system resource that remains
...@@ -994,10 +997,11 @@ pub const Dir = struct {...@@ -994,10 +997,11 @@ pub const Dir = struct {
994 w.RIGHT_PATH_REMOVE_DIRECTORY |997 w.RIGHT_PATH_REMOVE_DIRECTORY |
995 w.RIGHT_PATH_UNLINK_FILE;998 w.RIGHT_PATH_UNLINK_FILE;
996 }999 }
1000 const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW;
997 // TODO do we really need all the rights here?1001 // TODO do we really need all the rights here?
998 const inheriting: w.rights_t = w.RIGHT_ALL ^ w.RIGHT_SOCK_SHUTDOWN;1002 const inheriting: w.rights_t = w.RIGHT_ALL ^ w.RIGHT_SOCK_SHUTDOWN;
9991003
1000 const result = os.openatWasi(self.fd, sub_path, w.O_DIRECTORY, 0x0, base, inheriting);1004 const result = os.openatWasi(self.fd, sub_path, w.O_DIRECTORY, symlink_flags, base, inheriting);
1001 const fd = result catch |err| switch (err) {1005 const fd = result catch |err| switch (err) {
1002 error.FileTooBig => unreachable, // can't happen for directories1006 error.FileTooBig => unreachable, // can't happen for directories
1003 error.IsDir => unreachable, // we're providing O_DIRECTORY1007 error.IsDir => unreachable, // we're providing O_DIRECTORY
...@@ -1014,11 +1018,13 @@ pub const Dir = struct {...@@ -1014,11 +1018,13 @@ pub const Dir = struct {
1014 if (builtin.os.tag == .windows) {1018 if (builtin.os.tag == .windows) {
1015 const sub_path_w = try os.windows.cStrToPrefixedFileW(sub_path_c);1019 const sub_path_w = try os.windows.cStrToPrefixedFileW(sub_path_c);
1016 return self.openDirW(sub_path_w.span().ptr, args);1020 return self.openDirW(sub_path_w.span().ptr, args);
1017 } else if (!args.iterate) {1021 }
1022 const symlink_flags: u32 = if (args.no_follow) os.O_NOFOLLOW else 0x0;
1023 if (!args.iterate) {
1018 const O_PATH = if (@hasDecl(os, "O_PATH")) os.O_PATH else 0;1024 const O_PATH = if (@hasDecl(os, "O_PATH")) os.O_PATH else 0;
1019 return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC | O_PATH);1025 return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC | O_PATH | symlink_flags);
1020 } else {1026 } else {
1021 return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC);1027 return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC | symlink_flags);
1022 }1028 }
1023 }1029 }
10241030
...@@ -1030,7 +1036,7 @@ pub const Dir = struct {...@@ -1030,7 +1036,7 @@ pub const Dir = struct {
1030 const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |1036 const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
1031 w.SYNCHRONIZE | w.FILE_TRAVERSE;1037 w.SYNCHRONIZE | w.FILE_TRAVERSE;
1032 const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;1038 const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
1033 return self.openDirAccessMaskW(sub_path_w, flags);1039 return self.openDirAccessMaskW(sub_path_w, flags, args.no_follow);
1034 }1040 }
10351041
1036 /// `flags` must contain `os.O_DIRECTORY`.1042 /// `flags` must contain `os.O_DIRECTORY`.
...@@ -1050,7 +1056,7 @@ pub const Dir = struct {...@@ -1050,7 +1056,7 @@ pub const Dir = struct {
1050 return Dir{ .fd = fd };1056 return Dir{ .fd = fd };
1051 }1057 }
10521058
1053 fn openDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32) OpenError!Dir {1059 fn openDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, no_follow: bool) OpenError!Dir {
1054 const w = os.windows;1060 const w = os.windows;
10551061
1056 var result = Dir{1062 var result = Dir{
...@@ -1080,6 +1086,7 @@ pub const Dir = struct {...@@ -1080,6 +1086,7 @@ pub const Dir = struct {
1080 // implement this: https://git.midipix.org/ntapi/tree/src/fs/ntapi_tt_open_physical_parent_directory.c1086 // implement this: https://git.midipix.org/ntapi/tree/src/fs/ntapi_tt_open_physical_parent_directory.c
1081 @panic("TODO opening '..' with a relative directory handle is not yet implemented on Windows");1087 @panic("TODO opening '..' with a relative directory handle is not yet implemented on Windows");
1082 }1088 }
1089 const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0;
1083 var io: w.IO_STATUS_BLOCK = undefined;1090 var io: w.IO_STATUS_BLOCK = undefined;
1084 const rc = w.ntdll.NtCreateFile(1091 const rc = w.ntdll.NtCreateFile(
1085 &result.fd,1092 &result.fd,
...@@ -1090,7 +1097,7 @@ pub const Dir = struct {...@@ -1090,7 +1097,7 @@ pub const Dir = struct {
1090 0,1097 0,
1091 w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,1098 w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
1092 w.FILE_OPEN,1099 w.FILE_OPEN,
1093 w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT,1100 w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
1094 null,1101 null,
1095 0,1102 0,
1096 );1103 );
...@@ -1277,6 +1284,7 @@ pub const Dir = struct {...@@ -1277,6 +1284,7 @@ pub const Dir = struct {
1277 pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {1284 pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
1278 start_over: while (true) {1285 start_over: while (true) {
1279 var got_access_denied = false;1286 var got_access_denied = false;
1287
1280 // First, try deleting the item as a file. This way we don't follow sym links.1288 // First, try deleting the item as a file. This way we don't follow sym links.
1281 if (self.deleteFile(sub_path)) {1289 if (self.deleteFile(sub_path)) {
1282 return;1290 return;
...@@ -1297,7 +1305,8 @@ pub const Dir = struct {...@@ -1297,7 +1305,8 @@ pub const Dir = struct {
1297 error.Unexpected,1305 error.Unexpected,
1298 => |e| return e,1306 => |e| return e,
1299 }1307 }
1300 var dir = self.openDir(sub_path, .{ .iterate = true }) catch |err| switch (err) {1308
1309 var dir = self.openDir(sub_path, .{ .iterate = true, .no_follow = true }) catch |err| switch (err) {
1301 error.NotDir => {1310 error.NotDir => {
1302 if (got_access_denied) {1311 if (got_access_denied) {
1303 return error.AccessDenied;1312 return error.AccessDenied;
...@@ -1364,7 +1373,7 @@ pub const Dir = struct {...@@ -1364,7 +1373,7 @@ pub const Dir = struct {
1364 => |e| return e,1373 => |e| return e,
1365 }1374 }
13661375
1367 const new_dir = dir.openDir(entry.name, .{ .iterate = true }) catch |err| switch (err) {1376 const new_dir = dir.openDir(entry.name, .{ .iterate = true, .no_follow = true }) catch |err| switch (err) {
1368 error.NotDir => {1377 error.NotDir => {
1369 if (got_access_denied) {1378 if (got_access_denied) {
1370 return error.AccessDenied;1379 return error.AccessDenied;
lib/std/fs/test.zig+3-3
...@@ -36,7 +36,7 @@ test "readLinkAbsolute" {...@@ -36,7 +36,7 @@ test "readLinkAbsolute" {
3636
37 // Create symbolic link by path37 // Create symbolic link by path
38 try fs.symLinkAbsolute(target_path, symlink_path, .{});38 try fs.symLinkAbsolute(target_path, symlink_path, .{});
39 try testReadlinkAbsolute(target_path, symlink_path);39 try testReadLinkAbsolute(target_path, symlink_path);
40 }40 }
41 {41 {
42 const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" });42 const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" });
...@@ -44,11 +44,11 @@ test "readLinkAbsolute" {...@@ -44,11 +44,11 @@ test "readLinkAbsolute" {
4444
45 // Create symbolic link by path45 // Create symbolic link by path
46 try fs.symLinkAbsolute(target_path, symlink_path, .{ .is_directory = true });46 try fs.symLinkAbsolute(target_path, symlink_path, .{ .is_directory = true });
47 try testReadlinkAbsolute(target_path, symlink_path);47 try testReadLinkAbsolute(target_path, symlink_path);
48 }48 }
49}49}
5050
51fn testReadlinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {51fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
52 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;52 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
53 const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);53 const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);
54 testing.expect(mem.eql(u8, target_path, given));54 testing.expect(mem.eql(u8, target_path, given));
lib/std/os.zig+3-1
...@@ -1815,7 +1815,7 @@ pub fn unlinkatW(dirfd: fd_t, sub_path_w: [*:0]const u16, flags: u32) UnlinkatEr...@@ -1815,7 +1815,7 @@ pub fn unlinkatW(dirfd: fd_t, sub_path_w: [*:0]const u16, flags: u32) UnlinkatEr
18151815
1816 const want_rmdir_behavior = (flags & AT_REMOVEDIR) != 0;1816 const want_rmdir_behavior = (flags & AT_REMOVEDIR) != 0;
1817 const create_options_flags = if (want_rmdir_behavior)1817 const create_options_flags = if (want_rmdir_behavior)
1818 @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_DIRECTORY_FILE)1818 @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT)
1819 else1819 else
1820 @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_NON_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT); // would we ever want to delete the target instead?1820 @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_NON_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT); // would we ever want to delete the target instead?
18211821
...@@ -2390,9 +2390,11 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8...@@ -2390,9 +2390,11 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8
2390 else => |e| return e,2390 else => |e| return e,
2391 }2391 }
2392 };2392 };
2393 defer w.CloseHandle(handle);
23932394
2394 var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;2395 var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
2395 _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null);2396 _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null);
2397
2396 const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0]));2398 const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0]));
2397 switch (reparse_struct.ReparseTag) {2399 switch (reparse_struct.ReparseTag) {
2398 w.IO_REPARSE_TAG_SYMLINK => {2400 w.IO_REPARSE_TAG_SYMLINK => {
lib/std/os/windows.zig+1-9
...@@ -602,15 +602,7 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 {...@@ -602,15 +602,7 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 {
602 return buffer[0..end_index];602 return buffer[0..end_index];
603}603}
604604
605pub const CreateSymbolicLinkError = error{605pub const CreateSymbolicLinkError = error{ AccessDenied, PathAlreadyExists, FileNotFound, NameTooLong, InvalidUtf8, BadPathName, Unexpected };
606 AccessDenied,
607 PathAlreadyExists,
608 FileNotFound,
609 NameTooLong,
610 InvalidUtf8,
611 BadPathName,
612 Unexpected
613};
614606
615pub fn CreateSymbolicLink(607pub fn CreateSymbolicLink(
616 sym_link_path: []const u8,608 sym_link_path: []const u8,