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/
1111 ### Fixed
1212
1313 - 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
1415
1516 ## v0.8.1 - 2026-09-05
1617
+26, -1
......@@ -90,7 +90,15 @@ fn mkdirAll(io: std.Io, sub_dir_path: []const u8, permissions: std.Io.Dir.Permis
9090 var component = it.last() orelse return error.BadPathName;
9191 while (true) {
9292 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+ },
94102 error.FileNotFound => |e| {
95103 component = it.previous() orelse return e;
96104 continue;
......@@ -132,3 +140,20 @@ test "Cfg.init uses custom modes from env vars" {
132140 try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode);
133141 try std.testing.expectEqual(@as(u32, 0o660), cfg.log_mode);
134142 }
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+}