Commit ad8be3f
kylem0
·
2026-10-01 09:47:48 -0400 EDT
parent 6f80792
fix: prevent infinite loop on dangling symlinks (#272) When creating directory paths where an intermediate component is a broken symlink, mkdir returns EEXIST (PathAlreadyExists), but resolving child paths fails with ENOENT. Traversal ping-pongs between parent and child infinitely at 100% CPU. Validate that existing path components are actual directories before descending.
2 files changed,
+27,
-1
+1,
-0
| ... | ... | @@ -11,6 +11,7 @@ Use spec: https://common-changelog.org/ | |
| 11 | 11 | ### Fixed | |
| 12 | 12 | ||
| 13 | 13 | - Stop classifying complete kitty keyboard status replies (`CSI ? flags u`) as user input for leader selection | |
| 14 | + | - Prevent infinite loop in `mkdirAll` when directory path contains a dangling symlink | |
| 14 | 15 | ||
| 15 | 16 | ## v0.8.1 - 2026-09-05 | |
| 16 | 17 |
+26,
-1
| ... | ... | @@ -90,7 +90,15 @@ fn mkdirAll(io: std.Io, sub_dir_path: []const u8, permissions: std.Io.Dir.Permis | |
| 90 | 90 | var component = it.last() orelse return error.BadPathName; | |
| 91 | 91 | while (true) { | |
| 92 | 92 | std.Io.Dir.createDirAbsolute(io, component.path, permissions) catch |err| switch (err) { | |
| 93 | - | error.PathAlreadyExists => {}, | |
| 93 | + | error.PathAlreadyExists => { | |
| 94 | + | // Dangling symlinks return PathAlreadyExists on mkdir but fail on traversal, | |
| 95 | + | // causing an infinite loop between previous() and next(). | |
| 96 | + | const stat = std.Io.Dir.cwd().statFile(io, component.path, .{}) catch |e| switch (e) { | |
| 97 | + | error.FileNotFound => return error.NotDir, | |
| 98 | + | else => |stat_err| return stat_err, | |
| 99 | + | }; | |
| 100 | + | if (stat.kind != .directory) return error.NotDir; | |
| 101 | + | }, | |
| 94 | 102 | error.FileNotFound => |e| { | |
| 95 | 103 | component = it.previous() orelse return e; | |
| 96 | 104 | continue; |
| ... | ... | @@ -132,3 +140,20 @@ test "Cfg.init uses custom modes from env vars" { | |
| 132 | 140 | try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode); | |
| 133 | 141 | try std.testing.expectEqual(@as(u32, 0o660), cfg.log_mode); | |
| 134 | 142 | } | |
| 143 | + | ||
| 144 | + | test "mkdirAll fails on dangling symlink instead of looping" { | |
| 145 | + | var tmp = std.testing.tmpDir(.{}); | |
| 146 | + | defer tmp.cleanup(); | |
| 147 | + | ||
| 148 | + | const alloc = std.testing.allocator; | |
| 149 | + | const tmp_path = try tmp.dir.realPathFileAlloc(std.testing.io, ".", alloc); | |
| 150 | + | defer alloc.free(tmp_path); | |
| 151 | + | ||
| 152 | + | try tmp.dir.symLink(std.testing.io, "nonexistent", "broken_link", .{}); | |
| 153 | + | ||
| 154 | + | const target_path = try std.fs.path.join(alloc, &.{ tmp_path, "broken_link", "sub" }); | |
| 155 | + | defer alloc.free(target_path); | |
| 156 | + | ||
| 157 | + | const perms = std.Io.Dir.Permissions.fromMode(0o750); | |
| 158 | + | try std.testing.expectError(error.NotDir, mkdirAll(std.testing.io, target_path, perms)); | |
| 159 | + | } |