Linux Security Modules development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Josef Bacik" <josef@toxicpanda.com>,
	"Serge Hallyn" <serge@hallyn.com>,
	"Paul Moore" <paul@paul-moore.com>,
	"Christian Brauner" <brauner@kernel.org>
Cc: "James Morris" <jmorris@namei.org>,
	"David Howells" <dhowells@redhat.com>,
	"Jarkko Sakkinen" <jarkko@kernel.org>,
	"Andrew G. Morgan" <morgan@kernel.org>,
	"Serge Hallyn" <sergeh@kernel.org>,
	linux-security-module@vger.kernel.org, keyrings@vger.kernel.org,
	"Shuah Khan" <shuah@kernel.org>
Subject: Re: [PATCH v6 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces
Date: Mon, 26 Oct 2026 16:00:38 +0000	[thread overview]
Message-ID: <sashiko-outbox-185477@kernel.org> (raw)
In-Reply-To: <20261026-b4-setfcap-userns-v6-6-25f10b7ca497@toxicpanda.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] selftests/namespaces: unreachable set_fcap() and write_map() in setfcap_test
- [Medium] selftests/namespaces: fail instead of skip on ENOSYS in session_to_parent()
- [Medium] selftests/namespaces: missing parent session keyring join in r_new_session_keyring_maps()
--

commit ef6b694dab9e316eaa07835cf40f4ad86e6267ac
Author: Josef Bacik <josef@toxicpanda.com>

selftests/namespaces: test the CAP_SETFCAP rule for user namespaces

This commit adds the setfcap_test selftest to verify the CAP_SETFCAP rules for
mapping ancestor root users, writing file capabilities in user namespaces and
idmapped mounts, joining namespaces via setns(), and attaching via ptrace().

> diff --git a/tools/testing/selftests/namespaces/setfcap_test.c b/tools/testing/selftests/namespaces/setfcap_test.c
> new file mode 100644
> index 0000000000000..0a82b239b3685
> --- /dev/null
> +++ b/tools/testing/selftests/namespaces/setfcap_test.c

[ ... ]

> +static long x_joins_unshares_maps(void *a)
> +{
> +	long r;
> +
> +	restrict_root();
> +	r = join(PID(a));
> +	if (r)
> +		return r;
> +	if (unshare(CLONE_NEWUSER))
> +		return SETUP_ERROR;
> +	return write_map(0, "0 0 1\n");
> +}
> +
> +TEST_F(setfcap, map_after_setns_and_unshare)
> +{
> +	struct holder h;
> +
> +	h = spawn_mapped(HOLDER_ROOT, "0 0 1\n");
> +	ASSERT_GT(h.pid, 0);
> +	EXPECT_EQ(in_child(x_joins_unshares_maps, &h), -EPERM);
> +	reap_holder(h);
> +}

[ ... ]

> +static long session_to_parent(void *a)
> +{
> +	if (syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) < 0 ||
> +	    syscall(SYS_keyctl, KEYCTL_SESSION_TO_PARENT) < 0)
> +		return SETUP_ERROR;
> +	return 0;
> +}
> +
> +/* a cred replaced by key_change_session_keyring() keeps what it had */
> +static long r_new_session_keyring_maps(void *a)
> +{
> +	if (unshare(CLONE_NEWUSER))
> +		return SETUP_ERROR;
> +	if (in_child(session_to_parent, NULL))
> +		return SETUP_ERROR;
> +	return write_map(0, "0 0 1\n");
> +}
> +
> +TEST_F(setfcap, map_after_session_to_parent)
> +{
> +	EXPECT_EQ(in_child(r_new_session_keyring_maps, NULL), 0);
> +}

[Severity: Medium]
If the running kernel is built without CONFIG_KEYS, syscall(SYS_keyctl, ...)
in session_to_parent() fails with ENOSYS (or EOPNOTSUPP), which
r_new_session_keyring_maps() translates into SETUP_ERROR and causes
map_after_session_to_parent to fail EXPECT_EQ(..., 0).

Should map_after_session_to_parent check for ENOSYS / EOPNOTSUPP and call
SKIP() with a reason message when keyctl is not supported by the kernel?

[Severity: Medium]
Can KEYCTL_SESSION_TO_PARENT in session_to_parent() also fail with EPERM when
the test binary is invoked via sudo or su from an unprivileged login session?

In security/keys/keyctl.c:keyctl_session_to_parent(), the kernel requires the
parent's existing session keyring UID to match the child's effective UID:

	/* the keyrings must have the same UID */
	if ((pcred->session_keyring &&
	     !uid_eq(pcred->session_keyring->uid, mycred->euid)) ||
	    !uid_eq(mycred->session_keyring->uid, mycred->euid))
		goto unlock;

If the parent process in r_new_session_keyring_maps() inherited a session
keyring created for a non-zero UID before elevating privileges,
pcred->session_keyring->uid will not match mycred->euid (0), causing
KEYCTL_SESSION_TO_PARENT to fail and return SETUP_ERROR.

Should r_new_session_keyring_maps() call
syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) in the parent before
spawning session_to_parent() so the parent's session keyring is owned by
UID 0?

> +
> +/* ---------------------------------------------------------------------- */
> +/* File capabilities that are honoured in an ancestor                      */
> +
> +static long x_joins_sets_fcap(void *a)
> +{
> +	long r;
> +
> +	restrict_root();
> +	r = join(PID(a));
> +	if (r)
> +		return r;
> +	return set_fcap("/tmp/x", -1);
> +}
> +
> +TEST_F(setfcap, fcap_after_setns)
> +{
> +	struct holder h;
> +
> +	h = spawn_mapped(HOLDER_ROOT, "0 0 1\n");
> +	ASSERT_GT(h.pid, 0);
> +	EXPECT_EQ(in_child(x_joins_sets_fcap, &h), -EPERM);
> +	reap_holder(h);
> +	EXPECT_FALSE(user_gets_sys_admin("/tmp/x"));
> +}
> +
> +static long x_joins_sets_fcap_rootid5(void *a)
> +{
> +	long r;
> +
> +	restrict_root();
> +	r = join(PID(a));
> +	if (r)
> +		return r;
> +	return set_fcap("/tmp/x", 5);
> +}
> +
> +/* a v3 xattr can name the root user by any uid it has in the namespace */
> +TEST_F(setfcap, fcap_after_setns_v3_rootid)
> +{
> +	struct holder h;
> +
> +	h = spawn_mapped(HOLDER_ROOT, "5 0 1\n");
> +	ASSERT_GT(h.pid, 0);
> +	EXPECT_EQ(in_child(x_joins_sets_fcap_rootid5, &h), -EPERM);
> +	reap_holder(h);
> +	EXPECT_FALSE(user_gets_sys_admin("/tmp/x"));
> +}

[ ... ]

> +/* R maps "5 0 1", gives up CAP_SETFCAP, unshares again */
> +static long r_drops_then_nests(void *a)
> +{
> +	if (unshare(CLONE_NEWUSER) || map_both(0, "5 0 1\n"))
> +		return SETUP_ERROR;
> +	restrict_root();
> +	if (set_fcap("/tmp/x", 5) != -EPERM)
> +		return SETUP_ERROR;
> +	if (unshare(CLONE_NEWUSER))
> +		return SETUP_ERROR;
> +	if (write_map(0, "5 5 1\n"))
> +		return -EPERM;
> +	write_file(0, "setgroups", "deny");
> +	write_file(0, "gid_map", "5 5 1\n");
> +	return set_fcap("/tmp/x", 5);
> +}
> +
> +TEST_F(setfcap, fcap_after_dropping_setfcap_and_nesting)
> +{
> +	EXPECT_EQ(in_child(r_drops_then_nests, NULL), -EPERM);
> +	EXPECT_FALSE(user_gets_sys_admin("/tmp/x"));
> +}

[ ... ]

> +/* returns 1 to skip: no idmapped mounts on tmpfs */
> +static int idmap_case(const struct idmap_case *c, long *r)
> +{
> +	struct holder ns, im;
> +	int pid, p[2], st;
> +
> +	ns = spawn_mapped(HOLDER_ROOT, c->ns_map);
> +	if (ns.pid <= 0)
> +		return -1;
> +	im = c->idmap_map ? spawn_mapped(HOLDER_ROOT, c->idmap_map) : ns;
> +	if (im.pid <= 0 || new_file("/tmp/x", c->file_uid))
> +		return -1;
> +	if (idmap_tmp(im.pid)) {
> +		reap_holder(ns);
> +		if (c->idmap_map)
> +			reap_holder(im);
> +		return 1;
> +	}
> +	if (pipe(p))
> +		return -1;
> +	/* join from a child of the namespace's holder's sibling: pass the pid */
> +	pid = fork();
> +	if (!pid) {
> +		long res;
> +
> +		if (c->restricted)
> +			restrict_root();
> +		res = join(ns.pid);
> +		if (!res)
> +			res = set_fcap("/mnt/x", 0);
> +		if (write(p[1], &res, sizeof(res)) != sizeof(res))
> +			_exit(1);
> +		_exit(0);
> +	}
> +	if (read(p[0], r, sizeof(*r)) != sizeof(*r))
> +		*r = SETUP_ERROR;
> +	waitpid(pid, &st, 0);
> +	umount2("/mnt", MNT_DETACH);
> +	reap_holder(ns);
> +	if (c->idmap_map)
> +		reap_holder(im);
> +	return 0;
> +}
> +
> +FIXTURE(idmap) {};
> +
> +FIXTURE_VARIANT(idmap) {
> +	struct idmap_case c;
> +	long expect;
> +};
> +
> +/* X can't join a namespace that maps host uid 0 to get at the mount */
> +FIXTURE_VARIANT_ADD(idmap, reverse_mapping) {
> +	.c = { "0 0 1\n", "100000 0 1\n", 100000, 1 },
> +	.expect = -EPERM,
> +};

[Severity: Medium]
Are the set_fcap() and nested write_map() calls in several of these negative
tests reachable?

1. In map_after_setns_and_unshare (x_joins_unshares_maps()), fcap_after_setns
(x_joins_sets_fcap()), and fcap_after_setns_v3_rootid
(x_joins_sets_fcap_rootid5()), the target namespace h is created with
HOLDER_ROOT and maps host UID 0 (at UID 0 or UID 5). When the restricted child
calls join(PID(a)), setns() is rejected in userns_install() ->
cred_may_setfcap_userns() with -EPERM. Because those helpers immediately
return r when join() fails, the subsequent unshare(CLONE_NEWUSER) /
write_map() in x_joins_unshares_maps() and set_fcap() in x_joins_sets_fcap()
and x_joins_sets_fcap_rootid5() are never executed.

2. In fcap_after_dropping_setfcap_and_nesting (r_drops_then_nests()), after
mapping "5 0 1\n" in the first namespace, calling restrict_root(), and calling
unshare(CLONE_NEWUSER) a second time, write_map(0, "5 5 1\n") always fails
with -EPERM in verify_root_map() because UID 5 in the parent namespace maps
host UID 0. The check:

	if (write_map(0, "5 5 1\n"))
		return -EPERM;

always returns at that point (and also masks any non-EPERM error from
write_map()), leaving the rest of the function:

	write_file(0, "setgroups", "deny");
	write_file(0, "gid_map", "5 5 1\n");
	return set_fcap("/tmp/x", 5);

unreachable. As a result, neither fcap_after_setns_v3_rootid nor
fcap_after_dropping_setfcap_and_nesting reaches cap_convert_nscap() with a
non-zero v3 rootid (5) from a task that holds CAP_SETFCAP in its own user
namespace.

3. Similarly, in idmap_case() for the reverse_mapping variant, im is set up
with "100000 0 1\n" and mounted idmapped at /mnt, while ns is spawned with
HOLDER_ROOT and "0 0 1\n". When c->restricted is set, join(ns.pid) fails with
-EPERM in setns(), so set_fcap("/mnt/x", 0) is never called against the
idmapped mount. Because idmap_case() always spawns ns with HOLDER_ROOT,
cap_convert_nscap() is never exercised on an idmapped mount by a task in a
user namespace created without CAP_SETFCAP (such as one spawned with
HOLDER_RESTRICTED where parent root mapped the UIDs).

Should these tests spawn or enter aHOLDER_RESTRICTED namespace mapped by the
parent (or assert that setup steps like join() and write_map() succeed) so
that set_fcap() and the nested operations are actually reached?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261026-b4-setfcap-userns-v6-0-25f10b7ca497@toxicpanda.com?part=6

      reply	other threads:[~2026-10-26 16:00 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-26 15:41 [PATCH v6 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
2026-10-26 15:41 ` [PATCH v6 1/6] userns: record how far up CAP_SETFCAP reaches Josef Bacik
2026-10-26 15:48   ` sashiko-bot
2026-10-26 15:41 ` [PATCH v6 2/6] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
2026-10-26 15:54   ` sashiko-bot
2026-10-26 15:41 ` [PATCH v6 3/6] userns: check the opener and the writer before mapping uid 0 Josef Bacik
2026-10-26 16:00   ` sashiko-bot
2026-10-26 15:41 ` [PATCH v6 4/6] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
2026-10-26 16:05   ` sashiko-bot
2026-10-26 15:41 ` [PATCH v6 5/6] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
2026-10-26 15:56   ` sashiko-bot
2026-10-26 15:41 ` [PATCH v6 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces Josef Bacik
2026-10-26 16:00   ` sashiko-bot [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-185477@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=brauner@kernel.org \
    --cc=dhowells@redhat.com \
    --cc=jarkko@kernel.org \
    --cc=jmorris@namei.org \
    --cc=josef@toxicpanda.com \
    --cc=keyrings@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=morgan@kernel.org \
    --cc=paul@paul-moore.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=serge@hallyn.com \
    --cc=sergeh@kernel.org \
    --cc=shuah@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox