From nobody Sun Aug 23 19:09:57 2026 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hSkBZ0BqGz6phTx for ; Sun, 23 Aug 2026 19:09:58 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hSkBY6wvjz3HmN for ; Sun, 23 Aug 2026 19:09:57 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1787512197; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=/bbH527ggLSr34KNLrOMwkYC4BAaS7YS3wnVD3iSnMY=; b=SPZZc9xMYLAXzPGCG24Nw+05eWOkMr8CtHe5aw/VVVWWI9m5m4hslyC6BJpLq1WQWVB4n0 23DTV4wuLoVlZtNASerN38hORaKEHLlshouEbrR7kkY8ufAhCQwhJ56H6AWrzrqpGZBjwB FpJ9pJxOEbztXRdyCuT9n5b5DhnhYfynLdykceOTaqrvkBUYnuVMgk1xfAbXDaxBmlmerH 709TghFkml643YqhBbiNazwtoNi/Md8tRqZS2M9Ivh8e+I5nVZfMacJmqbnAwmGLj63uwZ +6AjyasgLfUJ4PawfiDDxwl8m6UNBBr3bApHJnjn0rmOvHwERl5obT1bj4yU3g== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1787512197; a=rsa-sha256; cv=none; b=YIeXnTnsY5Sgk68AKLoxHsyLFZJjuXCLX5iHHKT9XByb554G1XlE+FSNm9KZFM5uhuePvh LKrGdnuMramJli6cMROilX5950Oy29uGNlbDNT6kwS9hDHX3gyfHfG3lfytL+7Wr/rxqDx Lrqpz7K3dKLiQzo+IaIGzDZlYMG+Wu6GOpojP6aiA9mRIEteeH8dziXyM/K51LHCLpVJQ3 rWwp3v0FdqBr7igO7wd4HLP+0FdfOq3/enkaH1K881pHDgpj2HRgA8pg83OWGkji3ijAIN zdywMWx/vh6JOFcyMM589E9wesPWjxOgBWxPbn2ZT5q0F0oRFSvNUMHTqCgDzw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1787512197; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=/bbH527ggLSr34KNLrOMwkYC4BAaS7YS3wnVD3iSnMY=; b=bdqNEdQYGUuxZ0lxJlOFq83T0BdMGFuZF2oLcRQdulB8DXp+DRbSWsUnb5AhEK9oCfgNQt F4HU7F3YpV1GtvddmOA4xdznMpQlfWkMmOUwB7oAV5xoS2/FxwPGMd9HWBK/Mf80N0miAx YNgQObjWtwOE+WiqWi59ShY4mUUYI+CJ3GVFUwtVDUDsP7oAG/XSYZfKx2yaxhT5/QtbEN Wv2hExsXje2MhiBGCtKpQmn84rJf+6x7tdyfG5xUwtau1Tu9CZ7XagMuA1qXG0dATap1q7 ysMrDHwZmqFth+S6ofQKIDsTZb7g/wRPE7AkAnJb2R6yVHHtgdu8YTYCIUN/1A== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hSkBY5KddzVZS for ; Sun, 23 Aug 2026 19:09:57 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 36484 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Sun, 23 Aug 2026 19:09:57 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Cc: Jitendra Bhati From: Alan Somers Subject: git: 22df19c09af6 - main - chflags: use Capsicum capability mode List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: asomers X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 22df19c09af64dc8711ee62796ba4cf3e7e4824f Auto-Submitted: auto-generated Date: Sun, 23 Aug 2026 19:09:57 +0000 Message-Id: <6a8b4585.36484.c6bd332@gitrepo.freebsd.org> The branch main has been updated by asomers: URL: https://cgit.FreeBSD.org/src/commit/?id=22df19c09af64dc8711ee62796ba4cf3e7e4824f commit 22df19c09af64dc8711ee62796ba4cf3e7e4824f Author: Jitendra Bhati AuthorDate: 2026-08-18 21:46:01 +0000 Commit: Alan Somers CommitDate: 2026-08-23 19:02:00 +0000 chflags: use Capsicum capability mode Run the file-hierarchy traversal inside Capsicum capability mode using the fts_openat(3) API. This confines chflags to the directory hierarchies named on the command line, so a malicious or buggy tree cannot redirect it at files elsewhere via a crafted symbolic link. Because AT_FDCWD is rejected in capability mode, a directory descriptor for the parent directory of every argument with an absolute path or a path containing ".." is opened once before cap_enter(). Once every descriptor is open, cap_enter() is called and the traversal acts through fd-relative operations: chflagsat(fts_parent->fts_dirfd, fts_name). With -L chflags follows symbolic links, which may point outside the named hierarchy; chflag now rejects such accesses. The new --dereference-links-unsafely option disables the sandbox to restore the historical behavior for the rare callers that rely on it. But if the symbolic was link was named directly on the command line, chflags will still follow it (unless -h was given). Add functional tests covering relative, absolute, "..", recursive and mixed path arguments, and the symlink handling in both the default and --dereference-links-unsafely modes; they skip on filesystems that do not support the uchg flag. Sponsored by: Google LLC (GSoC 2026) Reviewed by: asomers Relnotes: yes (for the -L behavior change) Pull Request: https://github.com/freebsd/freebsd-src/pull/2375 --- bin/chflags/chflags.1 | 31 +++- bin/chflags/chflags.c | 296 ++++++++++++++++++++++++++++++-------- bin/chflags/tests/chflags_test.sh | 176 +++++++++++++++++++++++ 3 files changed, 440 insertions(+), 63 deletions(-) diff --git a/bin/chflags/chflags.1 b/bin/chflags/chflags.1 index 26b5bb24d9fe..521fe8370e3e 100644 --- a/bin/chflags/chflags.1 +++ b/bin/chflags/chflags.1 @@ -29,7 +29,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd July 9, 2026 +.Dd August 21, 2026 .Dt CHFLAGS 1 .Os .Sh NAME @@ -38,6 +38,7 @@ .Sh SYNOPSIS .Nm .Op Fl fhvx +.Op Fl -dereference-links-unsafely .Oo .Fl R .Op Fl H | Fl L | Fl P @@ -70,11 +71,20 @@ and hence unaffected by the command. If the .Ar file is a symbolic link, -change the file flags of the link itself rather than the file to which it points. +change the file flags of the link itself +rather than the file to which it points. .It Fl L If the .Fl R -option is specified, all symbolic links are followed. +option is specified, symbolic links are followed. +A symbolic link encountered during the traversal that points outside +the file hierarchy being traversed is not followed unless the +.Fl -dereference-links-unsafely +option is also given; +see the description of that option below. +A symbolic link named directly on the command line is followed unless +.Fl h +is specified. .It Fl P If the .Fl R @@ -97,6 +107,21 @@ option is specified more than once, the old and new flags of the file will also be printed, in octal notation. .It Fl x Do not cross mount points. +.It Fl -dereference-links-unsafely +By default, +.Nm +runs its traversal inside a +.Xr capsicum 4 +sandbox, which prevents it from following a symbolic link whose +target lies outside the file hierarchy named on the command line. +This option disables the sandbox so that such symbolic links are +followed, restoring historical behavior. +It is only meaningful together with +.Fl L +or the default logical behavior, and should be used with caution, +as it allows a crafted symbolic link to redirect +.Nm +at files outside the intended hierarchy. .El .Pp The flags are specified as an octal number or a comma separated list diff --git a/bin/chflags/chflags.c b/bin/chflags/chflags.c index 44bae3abe43b..7c96df0df520 100644 --- a/bin/chflags/chflags.c +++ b/bin/chflags/chflags.c @@ -30,13 +30,20 @@ */ #include +#include #include +#include +#include #include #include #include #include +#include +#include +#include #include +#include #include #include #include @@ -44,6 +51,8 @@ static volatile sig_atomic_t siginfo; +#define OPT_DEREF_UNSAFE (CHAR_MAX + 1) + static void usage(void) __dead2; static void @@ -53,19 +62,147 @@ siginfo_handler(int sig __unused) siginfo = 1; } -int -main(int argc, char *argv[]) +/* + * A path needs its own pre-opened directory descriptor unless it is a + * single path component that can be resolved directly relative to the + * base directory descriptor. Anything containing a '/' (an absolute + * path, or a relative path with a directory component) has its parent + * directory opened separately, so that in capability mode the final + * component is always reached relative to its immediate parent. + */ +static bool +needs_own_fd(const char *path) +{ + + return (strchr(path, '/') != NULL); +} + +/* + * Open a directory descriptor for the parent of "path", and return in + * "*base" a pointer to the final path component (relative to that + * descriptor). "*base" points into the storage of "path". + */ +static int +open_base(char *path, char **base) +{ + char *dir, *bn, *pathcopy; + int fd; + + /* + * dirname() and basename() may modify their argument and may + * return a pointer to internal storage, so operate on copies and + * duplicate basename()'s result for the caller. + */ + if ((pathcopy = strdup(path)) == NULL) + err(1, "strdup"); + dir = dirname(pathcopy); + fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + free(pathcopy); + + if ((pathcopy = strdup(path)) == NULL) + err(1, "strdup"); + bn = basename(pathcopy); + if ((*base = strdup(bn)) == NULL) + err(1, "strdup"); + free(pathcopy); + + return (fd); +} + +static int +chflags_fts(int dirfd, char **paths, int fts_options, u_long set, u_long clear, + int oct, int Rflag, int fflag, int vflag) { FTS *ftsp; FTSENT *p; - u_long clear, newflags, set; + u_long newflags; + int e, rval; + + if ((ftsp = fts_openat(dirfd, paths, fts_options, NULL)) == NULL) + err(1, NULL); + + for (rval = 0; errno = 0, (p = fts_read(ftsp)) != NULL;) { + int atflag; + + if ((fts_options & FTS_LOGICAL) || + ((fts_options & FTS_COMFOLLOW) && + p->fts_level == FTS_ROOTLEVEL)) + atflag = 0; + else + atflag = AT_SYMLINK_NOFOLLOW; + + switch (p->fts_info) { + case FTS_D: /* Change it at FTS_DP if we're recursive. */ + if (!Rflag) + fts_set(ftsp, p, FTS_SKIP); + continue; + case FTS_DNR: /* Warn, chflags. */ + warnx("%s: %s", p->fts_path, strerror(p->fts_errno)); + rval = 1; + break; + case FTS_ERR: /* Warn, continue. */ + case FTS_NS: + warnx("%s: %s", p->fts_path, strerror(p->fts_errno)); + rval = 1; + continue; + default: + break; + } + if (oct) + newflags = set; + else + newflags = (p->fts_statp->st_flags | set) & clear; + if (newflags == p->fts_statp->st_flags) + continue; + if (chflagsat(p->fts_parent->fts_dirfd, p->fts_name, newflags, + atflag) == -1) { + e = errno; + if (!fflag) { + warnc(e, "%s", p->fts_path); + rval = 1; + } + if (siginfo) { + (void)printf("%s: %s\n", p->fts_path, + strerror(e)); + siginfo = 0; + } + } else if (vflag || siginfo) { + (void)printf("%s", p->fts_path); + if (vflag > 1 || siginfo) + (void)printf(": 0%lo -> 0%lo", + (u_long)p->fts_statp->st_flags, + newflags); + (void)printf("\n"); + siginfo = 0; + } + } + if (errno) + err(1, "fts_read"); + (void)fts_close(ftsp); + return (rval); +} + +int +main(int argc, char *argv[]) +{ + static const struct option longopts[] = { + { "dereference-links-unsafely", no_argument, NULL, + OPT_DEREF_UNSAFE }, + { NULL, 0, NULL, 0 } + }; + u_long clear, set; long val; - int Hflag, Lflag, Rflag, fflag, hflag, vflag, xflag; - int ch, e, fts_options, oct, rval; + int Hflag, Lflag, Rflag, fflag, hflag, vflag, xflag, unsafe; + int ch, fts_options, oct, rval; + int cwd_fd, i, nrel, nown; + int *ownfd; + char **ownbase; char *flags, *ep; + char **relpaths, *twopath[2]; - Hflag = Lflag = Rflag = fflag = hflag = vflag = xflag = 0; - while ((ch = getopt(argc, argv, "HLPRfhvx")) != -1) + Hflag = Lflag = Rflag = fflag = hflag = vflag = xflag = unsafe = 0; + while ((ch = getopt_long(argc, argv, "HLPRfhvx", longopts, + NULL)) != -1) switch (ch) { case 'H': Hflag = 1; @@ -93,6 +230,9 @@ main(int argc, char *argv[]) case 'x': xflag = 1; break; + case OPT_DEREF_UNSAFE: + unsafe = 1; + break; case '?': default: usage(); @@ -145,66 +285,101 @@ main(int argc, char *argv[]) oct = 0; } - if ((ftsp = fts_open(++argv, fts_options , 0)) == NULL) - err(1, NULL); + argv++; + argc--; - for (rval = 0; errno = 0, (p = fts_read(ftsp)) != NULL;) { - int atflag; + /* + * Pre-open a directory descriptor for every path argument, so the + * traversal runs through fd-relative operations. Plain relative + * arguments share a descriptor for the current directory; absolute + * paths and paths containing ".." cannot be resolved relative to + * another descriptor in capability mode, so each gets its own + * parent descriptor. + */ + if ((cwd_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC)) < 0) + err(1, "."); - if ((fts_options & FTS_LOGICAL) || - ((fts_options & FTS_COMFOLLOW) && - p->fts_level == FTS_ROOTLEVEL)) - atflag = 0; - else - atflag = AT_SYMLINK_NOFOLLOW; + relpaths = calloc(argc + 1, sizeof(*relpaths)); + ownfd = calloc(argc, sizeof(*ownfd)); + ownbase = calloc(argc, sizeof(*ownbase)); + if (relpaths == NULL || ownfd == NULL || ownbase == NULL) + err(1, "calloc"); + nrel = 0; + nown = 0; + rval = 0; - switch (p->fts_info) { - case FTS_D: /* Change it at FTS_DP if we're recursive. */ - if (!Rflag) - fts_set(ftsp, p, FTS_SKIP); - continue; - case FTS_DNR: /* Warn, chflags. */ - warnx("%s: %s", p->fts_path, strerror(p->fts_errno)); - rval = 1; - break; - case FTS_ERR: /* Warn, continue. */ - case FTS_NS: - warnx("%s: %s", p->fts_path, strerror(p->fts_errno)); - rval = 1; - continue; - default: - break; - } - if (oct) - newflags = set; - else - newflags = (p->fts_statp->st_flags | set) & clear; - if (newflags == p->fts_statp->st_flags) - continue; - if (chflagsat(AT_FDCWD, p->fts_accpath, newflags, - atflag) == -1) { - e = errno; - if (!fflag) { - warnc(e, "%s", p->fts_path); + for (i = 0; i < argc; i++) { + char *arg = argv[i]; + char resolved[PATH_MAX]; + struct stat sb; + + /* + * A symbolic link named on the command line is followed + * unless -h was given (or a purely physical walk was + * requested). Its target may lie outside the link's parent + * directory, which capability mode could not reach, so + * resolve the link now and operate relative to the target's + * own parent. + */ + if ((fts_options & (FTS_LOGICAL | FTS_COMFOLLOW)) && + lstat(arg, &sb) == 0 && S_ISLNK(sb.st_mode)) { + if (realpath(arg, resolved) == NULL) { + warn("%s", arg); rval = 1; + continue; } - if (siginfo) { - (void)printf("%s: %s\n", p->fts_path, - strerror(e)); - siginfo = 0; + arg = resolved; + } + + if (needs_own_fd(arg)) { + int fd = open_base(arg, &ownbase[nown]); + if (fd < 0) { + warn("%s", argv[i]); + rval = 1; + continue; } - } else if (vflag || siginfo) { - (void)printf("%s", p->fts_path); - if (vflag > 1 || siginfo) - (void)printf(": 0%lo -> 0%lo", - (u_long)p->fts_statp->st_flags, - newflags); - (void)printf("\n"); - siginfo = 0; + ownfd[nown] = fd; + nown++; + } else { + /* + * A resolved symlink is always absolute and thus + * takes the branch above; only an unmodified argument + * reaches this point. Store argv[i], which outlives + * the on-stack resolved[] buffer. + */ + assert(arg != resolved); + relpaths[nrel++] = argv[i]; } } - if (errno) - err(1, "fts_read"); + relpaths[nrel] = NULL; + + if (caph_limit_stdio() < 0) + err(1, "caph_limit_stdio"); + /* + * With --dereference-links-unsafely the traversal may follow a + * symlink to a file outside the hierarchy named on the command + * line, which capability mode would block, so skip caph_enter() in + * that case. + */ + if (!unsafe && caph_enter() < 0) + err(1, "caph_enter"); + + /* Process all plain relative paths together under cwd_fd. */ + if (nrel > 0) + rval |= chflags_fts(cwd_fd, relpaths, fts_options, set, clear, + oct, Rflag, fflag, vflag); + + /* Process each absolute / ".."-containing path under its own fd. */ + for (i = 0; i < nown; i++) { + twopath[0] = ownbase[i]; + twopath[1] = NULL; + rval |= chflags_fts(ownfd[i], twopath, fts_options, set, + clear, oct, Rflag, fflag, vflag); + free(ownbase[i]); + } + free(relpaths); + free(ownfd); + free(ownbase); exit(rval); } @@ -212,6 +387,7 @@ static void usage(void) { (void)fprintf(stderr, - "usage: chflags [-fhvx] [-R [-H | -L | -P]] flags file ...\n"); + "usage: chflags [-fhvx] [-R [-H | -L | -P]] " + "[--dereference-links-unsafely] flags file ...\n"); exit(1); } diff --git a/bin/chflags/tests/chflags_test.sh b/bin/chflags/tests/chflags_test.sh index 0fb3f31259b0..29c489b9a755 100644 --- a/bin/chflags/tests/chflags_test.sh +++ b/bin/chflags/tests/chflags_test.sh @@ -1,5 +1,6 @@ # # Copyright 2017 Shivansh Rai +# Copyright (c) 2026 Jitendra Bhati # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,6 +28,18 @@ usage_output='usage: chflags' +# Skip the calling test if the work filesystem does not support +# setting the nodump file flag (e.g. some ZFS configurations). +require_chflags() +{ + touch .chflags_probe + if ! chflags nodump .chflags_probe 2>/dev/null; then + rm -f .chflags_probe + atf_skip "filesystem does not support the nodump flag" + fi + rm -f .chflags_probe +} + atf_test_case invalid_usage invalid_usage_head() { @@ -55,8 +68,171 @@ no_arguments_body() atf_check -s not-exit:0 -e match:"$usage_output" chflags } +atf_test_case relative_path +relative_path_head() +{ + atf_set "descr" "chflags sets flags on a relative path" +} +relative_path_body() +{ + require_chflags + touch file + atf_check chflags nodump file + atf_check -o match:nodump stat -f "%Sf" file +} + +atf_test_case absolute_path +absolute_path_head() +{ + atf_set "descr" "chflags sets flags on an absolute path" +} +absolute_path_body() +{ + require_chflags + touch file + atf_check chflags nodump "$(pwd)/file" + atf_check -o match:nodump stat -f "%Sf" file +} + +atf_test_case dotdot_path +dotdot_path_head() +{ + atf_set "descr" "chflags sets flags on a path containing .." +} +dotdot_path_body() +{ + require_chflags + mkdir dir + touch file + cd dir + atf_check chflags nodump ../file + atf_check -o match:nodump stat -f "%Sf" ../file +} + +atf_test_case recursive +recursive_head() +{ + atf_set "descr" "chflags -R sets flags on a directory tree" +} +recursive_body() +{ + require_chflags + mkdir -p dir/sub + touch dir/file dir/sub/file + atf_check chflags -R nodump dir + atf_check -o match:nodump stat -f "%Sf" dir/file + atf_check -o match:nodump stat -f "%Sf" dir/sub/file +} + +atf_test_case mixed_paths +mixed_paths_head() +{ + atf_set "descr" "chflags handles relative, absolute and .. paths together" +} +mixed_paths_body() +{ + require_chflags + mkdir dir + touch a dir/b c + cd dir + atf_check chflags nodump b "$(pwd)/../a" ../c + atf_check -o match:nodump stat -f "%Sf" b + atf_check -o match:nodump stat -f "%Sf" ../a + atf_check -o match:nodump stat -f "%Sf" ../c +} + +atf_test_case outside_symlink_rejected +outside_symlink_rejected_head() +{ + atf_set "descr" "chflags -RL does not follow a symlink pointing " \ + "outside the traversal by default" +} +outside_symlink_rejected_body() +{ + require_chflags + mkdir -p foo/bar/baz + touch target + ln -s ../../../target foo/bar/baz/link + atf_check -s not-exit:0 -e ignore chflags -RL nodump foo/bar + atf_check -o match:"target[[:space:]]*-" stat -f "%N %Sf" target +} + +atf_test_case outside_symlink_unsafe +outside_symlink_unsafe_head() +{ + atf_set "descr" "chflags -RL --dereference-links-unsafely follows " \ + "a symlink pointing outside the traversal" +} +outside_symlink_unsafe_body() +{ + require_chflags + mkdir -p foo/bar/baz + touch target + ln -s ../../../target foo/bar/baz/link + atf_check chflags -RL --dereference-links-unsafely nodump foo/bar + atf_check -o match:nodump stat -f "%Sf" target +} + +atf_test_case symlink_no_h +symlink_no_h_head() +{ + atf_set "descr" "chflags on a symlink without -h changes the target" +} +symlink_no_h_body() +{ + require_chflags + mkdir dir + touch target + ln -s ../target dir/link + atf_check chflags nodump dir/link + atf_check -o match:nodump stat -f "%Sf" target + atf_check -o not-match:nodump stat -f "%Sf" dir/link +} + +atf_test_case symlink_rootlevel_H +symlink_rootlevel_H_head() +{ + atf_set "descr" "chflags -RH follows a symbolic link to a" \ + " directory named on the command line" +} +symlink_rootlevel_H_body() +{ + require_chflags + mkdir dir realdir + touch realdir/file + ln -s ../realdir dir/dlink + atf_check chflags -RH nodump dir/dlink + atf_check -o match:nodump stat -f "%Sf" realdir/file + atf_check -o not-match:nodump stat -f "%Sf" dir/dlink +} + +atf_test_case symlink_rootlevel_H_dead +symlink_rootlevel_H_dead_head() +{ + atf_set "descr" "chflags -RH on a broken symbolic link fails" \ + " gracefully" +} +symlink_rootlevel_H_dead_body() +{ + require_chflags + mkdir dir + ln -s ../nonexistent dir/dlink + atf_check -s not-exit:0 -e match:"No such file or directory" \ + chflags -RH nodump dir/dlink +} + atf_init_test_cases() { atf_add_test_case invalid_usage atf_add_test_case no_arguments + atf_add_test_case relative_path + atf_add_test_case absolute_path + atf_add_test_case dotdot_path + atf_add_test_case recursive + atf_add_test_case mixed_paths + atf_add_test_case symlink_no_h + atf_add_test_case symlink_rootlevel_H + atf_add_test_case symlink_rootlevel_H_dead + atf_add_test_case outside_symlink_rejected + atf_add_test_case outside_symlink_unsafe }