chflags -RL and symlinks pointing outside the traversal: preserve behavior or add a Capsicum guardrail?

From: Jitendra Bhati <jitendrabhati2026_at_gmail.com>
Date: Wed, 19 Aug 2026 14:37:03 UTC
Hi all,

As part of GSoC 2026 I'm capsicumizing chflags(1), using the new
fts_openat(3) API:

https://github.com/freebsd/freebsd-src/pull/2375

This raises a backward-compatibility question that Alan Somers and I think
warrants wider input.

In physical modes (-P, -h, and the default without -L) the traversal now
runs inside Capsicum capability mode: chflags pre-opens a directory
descriptor per argument, calls cap_enter(), and operates via:

chflagsat(fts_parent->fts_dirfd, fts_name, ...);

This confines chflags to the hierarchies named on the command line.

The complication is -L (FTS_LOGICAL), where chflags follows symlinks. A
followed symlink can point outside the traversal root, e.g.:

file
foo/bar/baz/link -> ../../../file

Running:

chflags -RL <flags> foo/bar

today follows link and changes the flags on file, which lives outside
foo/bar. Capability mode would block that access, because file isn't
reachable from the descriptor opened for foo/bar.

For now the PR preserves existing behavior: when -L is in effect it skips
cap_enter() and keeps the historical path-based call:

chflagsat(AT_FDCWD, p->fts_accpath, ...);

So -L is not capsicumized, but nothing regresses.

The open question: is "following a symlink out of the named hierarchy"
behavior worth preserving, or is it exactly the kind of thing a
Capsicum-ized chflags should refuse? A tree that redirects chflags at
arbitrary files via a crafted symlink is arguably a threat we'd want to
guard against. But it's also a long-standing, documented -L semantic, and
changing it would be a POLA violation for anyone relying on it.

Options:

Keep -L as-is (path-based, not confined) - current PR behavior.
Capsicumize -L too, and treat out-of-hierarchy symlink targets as an error.
Capsicumize -L but keep out-of-hierarchy targets working by opening a
descriptor for the resolved target's parent (loses most of the confinement
benefit for -L).

I lean toward (1) for this PR and revisiting -L separately, but I'd
appreciate opinions on whether the stricter behavior is worth the
compatibility break.

Thanks,
Jitendra Bhati
ᐧ