commit: 3222081dd068d490c610c2f79797d6f53374a862
parent 08147ec21ddca45950d806e72ebfba30f44dc3c3
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Sat, 13 Dec 2025 16:29:53 +0100
cmd/ln: Don't skip directory check prior to unlinking with -T
Done because unlink() returning EISDIR is a Linux extension,
and EPERM (BSD, POSIX.1-2024) is too generic an error.
Smells like a TOCTOU trap to me but it is what it is…
Diffstat:
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/cmd/ln.c b/cmd/ln.c
@@ -68,7 +68,7 @@ do_link(char *src, char *dest, int destfd)
return -1;
}
- if(destfd < 0 && !opt_T)
+ if(destfd < 0)
{
destfd = open(dest, open_dest_flags);
if(destfd < 0)
@@ -95,8 +95,14 @@ do_link(char *src, char *dest, int destfd)
}
int dirfd = AT_FDCWD;
- if(S_ISDIR(dest_stat.st_mode) && !opt_T)
+ if(S_ISDIR(dest_stat.st_mode))
{
+ if(opt_T)
+ {
+ fprintf(stderr, "ln: error: Option -T passed but target '%s' is a directory\n", dest);
+ return -1;
+ }
+
dirfd = destfd;
}
else
diff --git a/test-cmd/ln.sh b/test-cmd/ln.sh
@@ -126,7 +126,7 @@ t_cmd Ts:cleanup '' rm Ts
t_cmd Ts.d:mkdir '' mkdir -p Ts.d
t_args --exit=1 Ts.d "ln: error: Destination 'Ts.d' already exists
" -Ts / Ts.d
-t_args --exit=1 Ts.d:f "ln: error: Failed unlinking destination 'Ts.d': Is a directory
+t_args --exit=1 Ts.d:f "ln: error: Option -T passed but target 'Ts.d' is a directory
" -Tsf / Ts.d
t_cmd Ts.d:rmdir '' rm -r Ts.d
@@ -139,6 +139,6 @@ t_cmd T.d:mkdir '' mkdir -p T.d
t_args --exit=1 T.d "ln: error: Destination 'T.d' already exists
" -T "$target" T.d
t_cmd '' '' test -d T.d
-t_args --exit=1 T.d:f "ln: error: Failed unlinking destination 'T.d': Is a directory
+t_args --exit=1 T.d:f "ln: error: Option -T passed but target 'T.d' is a directory
" -Tf "$target" T.d
t_cmd T.d:rmdir '' rm -r T.d