logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/
commit: cd06343865e358791e5b40c883224a5495f0143a
parent 85835133a4c2cd5a224bb91f2ba938e19d6a1317
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Fri,  1 Aug 2025 07:17:59 +0200

cmd/ln: correctly handle broken symlinks

O_PATH simplifies things a lot, for example it opens the symbolic
link when O_NOFOLLOW is passed.

Diffstat:

Mcmd/ln.c178++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mtest-cmd/ln.sh9++++++++-
2 files changed, 120 insertions(+), 67 deletions(-)

diff --git a/cmd/ln.c b/cmd/ln.c @@ -2,34 +2,31 @@ // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> // SPDX-License-Identifier: MPL-2.0 -#define _POSIX_C_SOURCE 200809L +#define _DEFAULT_SOURCE // due to O_PATH +// Don't define _POSIX_C_SOURCE otherwise FreeBSD hides O_PATH #include "../lib/bitmasks.h" #include "../libutils/getopt_nolong.h" -// NetBSD (9.3 and 10) hides symlink behind _XOPEN_SOURCE / _NETBSD_SOURCE -#ifdef __NetBSD__ -#define _XOPEN_SOURCE 700 -#endif - #include <errno.h> #include <fcntl.h> // linkat, AT_SYMLINK_FOLLOW #include <libgen.h> // basename #include <limits.h> // PATH_MAX #include <stdbool.h> -#include <stdio.h> // fprintf -#include <string.h> // strerror -#include <sys/stat.h> -#include <unistd.h> // getopt, symlink, link +#include <stdio.h> // fprintf +#include <string.h> // strerror +#include <sys/stat.h> // fstat +#include <unistd.h> // getopt, symlink, link const char *argv0 = "ln"; static bool opt_s = false, force = false; static int link_flags = 0; -static int open_dir_flags = O_RDONLY | O_DIRECTORY; +static int open_dest_flags = O_RDONLY | O_PATH; +static struct stat dest_stat; static int -do_link(char *src, char *dest) +do_link(char *src, char *dest, int destfd) { if(opt_s) { @@ -56,52 +53,78 @@ do_link(char *src, char *dest) } } - errno = 0; - int dirfd = open(dest, open_dir_flags); - if(dirfd < 0) + if(strcmp(src, dest) == 0) { - switch(errno) - { - case ENOENT: - break; - // ENOTDIR: Found but not a directory - case ENOTDIR: - // ELOOP: POSIX return code when O_NOFOLLOW encounters a symbolic link - case ELOOP: - // EMLINK: Same as ELOOP but FreeBSD *sigh* - case EMLINK: -#ifdef EFTYPE - // EFTYPE: Same as ELOOP but NetBSD *grunt* - case EFTYPE: -#endif - if(!force) - { - fprintf(stderr, "ln: error: Destination '%s' already exists\n", dest); - return -1; - } + fprintf(stderr, "ln: error: Path '%s' passed to both source and destination\n", src); + return 1; + } - errno = 0; + errno = 0; - dirfd = AT_FDCWD; + if(!force) + { + fprintf(stderr, "ln: error: Destination '%s' already exists\n", dest); + return -1; + } - if(unlink(dest) < 0) + if(destfd < 0) + { + destfd = open(dest, open_dest_flags); + if(destfd < 0) + { + if(errno != ENOENT) { fprintf(stderr, - "ln: error: Failed removing already existing destination '%s': %s\n", + "ln: error: Failed opening destination as path '%s': %s\n", dest, strerror(errno)); return -1; } - break; - default: + } + else if(fstat(destfd, &dest_stat) < 0) + { fprintf(stderr, - "ln: error: Failed opening destination as directory '%s': %s\n", + "ln: error: Failed getting status for destination '%s': %s\n", dest, strerror(errno)); return -1; } } + int dirfd = AT_FDCWD; + if(S_ISDIR(dest_stat.st_mode)) + { + dirfd = destfd; + } + else + { + /* check if symbolic/hard link refers to same file */ + struct stat src_stat; + if(stat(src, &src_stat) < 0) + { + if(errno != ENOENT) + { + fprintf( + stderr, "ln: error: Failed getting status for source '%s': %s\n", src, strerror(errno)); + return -1; + } + } + else if(src_stat.st_dev == dest_stat.st_dev && src_stat.st_ino == dest_stat.st_ino) + { + fprintf(stderr, + "ln: error: Source '%s' and destination '%s' refer to the same file\n", + src, + dest); + return -1; + } + + if(unlink(dest) < 0) + { + fprintf(stderr, "ln: error: Failed unlinking destination '%s': %s\n", dest, strerror(errno)); + return -1; + } + } + if(opt_s) { if(symlinkat(src, dirfd, dest) == 0) goto cleanup; @@ -122,11 +145,14 @@ do_link(char *src, char *dest) } cleanup: - if(dirfd == AT_FDCWD) return 0; + if(destfd < 0) return 0; - if(close(dirfd) != 0) + if(close(destfd) != 0) { - fprintf(stderr, "ln: error: Failed closing directory '%s': %s\n", dest, strerror(errno)); + fprintf(stderr, + "ln: error: Failed closing destination directory '%s': %s\n", + dest, + strerror(errno)); return -1; } @@ -137,8 +163,8 @@ static void usage(void) { fprintf(stderr, "\ -Usage: ln [-fv] [-L|-P] source... target\n\ - ln -s [-fv] reference... target\n\ +Usage: ln [-fnv] [-L|-P] source... target\n\ + ln -s [-fnv] reference... target\n\ "); } @@ -155,7 +181,7 @@ main(int argc, char *argv[]) force = true; break; case 'n': - FIELD_SET(open_dir_flags, O_NOFOLLOW); + FIELD_SET(open_dest_flags, O_NOFOLLOW); break; case 's': opt_s = true; @@ -179,8 +205,8 @@ main(int argc, char *argv[]) argc -= optind; argv += optind; - char *dest = argv[argc - 1]; - char target[PATH_MAX] = ""; + char *target = argv[argc - 1]; + char dest[PATH_MAX] = ""; if(argc <= 0) { @@ -189,25 +215,42 @@ main(int argc, char *argv[]) } else if(argc == 1) { - dest = (char *)"."; + target = (char *)"."; argc++; } else if(argc == 2) { - errno = 0; - struct stat dest_status; - int ret_stat = fstatat(AT_FDCWD, argv[1], &dest_status, AT_SYMLINK_NOFOLLOW); - if( - // clang-format off - argc == 2 && ( - (ret_stat != 0 && errno == ENOENT) || - (ret_stat == 0 && !S_ISDIR(dest_status.st_mode)) - ) - // clang-format on - ) + int targetfd = open(target, open_dest_flags); + if(targetfd >= 0) + { + if(fstat(targetfd, &dest_stat) < 0) + { + fprintf(stderr, + "ln: error: Failed getting status for target '%s': %s\n", + dest, + strerror(errno)); + return -1; + } + + if(!S_ISDIR(dest_stat.st_mode)) + { + int ret = do_link(argv[0], argv[1], targetfd); + + return ret < 0 ? 1 : 0; + } + + if(close(targetfd) < 0) + { + fprintf(stderr, + "ln: error: Failed closing target directory '%s': %s\n", + target, + strerror(errno)); + return -1; + } + } + else { - errno = 0; - int ret = do_link(argv[0], argv[1]); + int ret = do_link(argv[0], argv[1], -1); return ret < 0 ? 1 : 0; } @@ -218,13 +261,16 @@ main(int argc, char *argv[]) char *src = argv[i]; char *src_basename = basename(src); - if(snprintf(target, PATH_MAX, "%s/%s", dest, src_basename) < 0) + if(snprintf(dest, PATH_MAX, "%s/%s", target, src_basename) < 0) { - fprintf(stderr, "ln: error: Failed joining destination '%s' and target '%s'\n", dest, src); + fprintf(stderr, + "ln: error: Failed joining target '%s' and source basename '%s'\n", + target, + src_basename); return 1; } - if(do_link(src, target) < 0) return 1; + if(do_link(src, dest, -1) < 0) return 1; if(verbose) printf("'%s' -> '%s'\n", src, dest); } diff --git a/test-cmd/ln.sh b/test-cmd/ln.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> # SPDX-License-Identifier: MPL-2.0 -plans=55 +plans=59 WD=$(dirname "$0") target="${WD}/../cmd/ln" . "${WD}/tap.sh" @@ -93,3 +93,10 @@ t_cmd '' '' rm -r e_target_dir e_src_dir t_args 'implicit_dest' '' -sn //example.org t_readlink ./example.org //example.org t_cmd '' '' rm ./example.org + +t_args 'dest_broken_symlink:create' '' -s /var/empty/e/no/ent dest_broken_symlink +t_args --exit=1 'dest_broken_symlink' "\ +ln: error: Destination 'dest_broken_symlink' already exists +" -s //example.org dest_broken_symlink +t_args 'dest_broken_symlink:force' '' -sf //example.org dest_broken_symlink +t_cmd 'dest_broken_symlink:cleanup' '' rm dest_broken_symlink