Kernel keyring 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 v7 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces
Date: Mon, 26 Oct 2026 21:22:52 +0000	[thread overview]
Message-ID: <sashiko-outbox-185749@kernel.org> (raw)
In-Reply-To: <20261026-b4-setfcap-userns-v7-6-bf8a576f7259@toxicpanda.com>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests/namespaces: unclosed pipe ends and missing kill in setfcap_test
--

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

selftests/namespaces: test the CAP_SETFCAP rule for user namespaces

This commit adds kselftest coverage in tools/testing/selftests/namespaces/
for the CAP_SETFCAP rule governing user namespace UID mappings, file
capabilities, setns(), idmapped mounts, and ptrace attach checks.

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

[ ... ]

> +static long x_nonzero_root(void *a)
> +{
> +	int s1[2], s2[2], res[2], pid;
> +	long r = SETUP_ERROR;
> +	char c;
> +
> +	if (pipe(s1) || pipe(s2) || pipe(res))
> +		return SETUP_ERROR;
> +	pid = fork();
> +	if (!pid) {
> +		restrict_root();
> +		if (unshare(CLONE_NEWUSER) || write(s1[1], "u", 1) != 1 ||
> +		    read(s2[0], &c, 1) != 1)
> +			_exit(1);
> +		if (unshare(CLONE_NEWUSER))
> +			r = SETUP_ERROR;
> +		else
> +			r = write_map(0, "0 1000 1\n");
> +		if (write(res[1], &r, sizeof(r)) != sizeof(r))
> +			_exit(1);
> +		_exit(0);
> +	}
> +	if (read(s1[0], &c, 1) != 1 || map_both(pid, "1000 0 1\n") ||
> +	    write(s2[1], "g", 1) != 1 || read(res[0], &r, sizeof(r)) != sizeof(r))
> +		r = SETUP_ERROR;
> +	waitpid(pid, NULL, 0);
> +	return r;
> +}
> +
> +TEST_F(setfcap, map_ancestor_root_at_nonzero_uid)
> +{
> +	EXPECT_EQ(in_child(x_nonzero_root, NULL), -EPERM);
> +}
> +
> +/*
> + * R maps host uid 0 as uid 5 of its namespace and forks X, which gives up
> + * CAP_SETFCAP there and unshares again.  "5 5 1" then maps host uid 0 one
> + * level down, as uid 5.  X writes it itself, or R does from the parent and X
> + * writes a v3 xattr with rootid 5.
> + */
> +static long r_nests_x(void *a)
> +{
> +	int x_maps = *(int *)a;
> +	int s1[2], s2[2], res[2], pid;
> +	long r = SETUP_ERROR;
> +	char c;
> +
> +	if (unshare(CLONE_NEWUSER) || map_both(0, "5 0 1\n"))
> +		return SETUP_ERROR;
> +	if (pipe(s1) || pipe(s2) || pipe(res))
> +		return SETUP_ERROR;
> +	pid = fork();
> +	if (!pid) {
> +		restrict_root();
> +		if (unshare(CLONE_NEWUSER))
> +			r = SETUP_ERROR;
> +		else if (x_maps)
> +			r = write_map(0, "5 5 1\n");
> +		else if (write(s1[1], "u", 1) != 1 || read(s2[0], &c, 1) != 1)
> +			r = SETUP_ERROR;
> +		else
> +			r = set_fcap("/tmp/x", 5);
> +		if (write(res[1], &r, sizeof(r)) != sizeof(r))
> +			_exit(1);
> +		_exit(0);
> +	}
> +	if (!x_maps && (read(s1[0], &c, 1) != 1 || map_both(pid, "5 5 1\n") ||
> +			write(s2[1], "g", 1) != 1))
> +		r = SETUP_ERROR;
> +	else if (read(res[0], &r, sizeof(r)) != sizeof(r))
> +		r = SETUP_ERROR;
> +	kill(pid, SIGKILL);
> +	waitpid(pid, NULL, 0);
> +	return r;
> +}

[ ... ]

> +static long dropped_writes_inherited_fd(void *a)
> +{
> +	int fd, pid, st;
> +	long r = SETUP_ERROR;
> +	int p[2];
> +
> +	if (unshare(CLONE_NEWUSER))
> +		return SETUP_ERROR;
> +	fd = open("/proc/self/uid_map", O_WRONLY);
> +	if (fd < 0 || pipe(p))
> +		return SETUP_ERROR;
> +	pid = fork();
> +	if (!pid) {
> +		drop_setfcap_keep_bset();
> +		r = wr(fd, "0 0 1\n");
> +		if (write(p[1], &r, sizeof(r)) != sizeof(r))
> +			_exit(1);
> +		_exit(0);
> +	}
> +	if (read(p[0], &r, sizeof(r)) != sizeof(r))
> +		r = SETUP_ERROR;
> +	waitpid(pid, &st, 0);
> +	return r;
> +}

[ ... ]

> +static long x_unshares_r_maps(void *a)
> +{
> +	const struct parent_maps *m = a;
> +	int s1[2], s2[2], res[2], pid;
> +	long r = SETUP_ERROR;
> +	char c;
> +
> +	if (pipe(s1) || pipe(s2) || pipe(res))
> +		return SETUP_ERROR;
> +	pid = fork();
> +	if (!pid) {
> +		restrict_root();
> +		if (unshare(CLONE_NEWUSER) || write(s1[1], "u", 1) != 1 ||
> +		    read(s2[0], &c, 1) != 1)
> +			_exit(1);
> +		if (m->own_tmpfs) {
> +			if (unshare(CLONE_NEWNS) ||
> +			    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) ||
> +			    mount("tmpfs", "/mnt", "tmpfs", 0, NULL) ||
> +			    new_file("/mnt/f", 0))
> +				r = SETUP_ERROR;
> +			else
> +				r = set_fcap("/mnt/f", m->rootid);
> +		} else {
> +			r = set_fcap("/tmp/x", m->rootid);
> +		}
> +		if (write(res[1], &r, sizeof(r)) != sizeof(r))
> +			_exit(1);
> +		_exit(0);
> +	}
> +	if (read(s1[0], &c, 1) != 1 || map_both(pid, m->map) ||
> +	    write(s2[1], "g", 1) != 1 || read(res[0], &r, sizeof(r)) != sizeof(r))
> +		r = SETUP_ERROR;
> +	kill(pid, SIGKILL);
> +	waitpid(pid, NULL, 0);
> +	return r;
> +}

[ ... ]

> +TEST_F(setfcap, fcap_v2_without_uid0_from_parent)
> +{
> +	int ready[2], go[2], mapped[2], pid;
> +	struct holder h;
> +	char c;
> +
> +	ASSERT_EQ(pipe(ready), 0);
> +	ASSERT_EQ(pipe(go), 0);
> +	ASSERT_EQ(pipe(mapped), 0);
> +	pid = fork();
> +	ASSERT_GE(pid, 0);
> +	if (!pid) {
> +		/* host uid 0 is uid 1000 in here, nobody is uid 0 */
> +		if (unshare(CLONE_NEWUSER) || write(ready[1], "u", 1) != 1 ||
> +		    read(mapped[0], &c, 1) != 1 || unshare(CLONE_NEWNS) ||
> +		    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) ||
> +		    mount("tmpfs", "/mnt", "tmpfs", 0, NULL) ||
> +		    new_file("/mnt/f", 1000) || write(ready[1], "m", 1) != 1)
> +			_exit(1);
> +		if (read(go[0], &c, 1) < 0)
> +			_exit(1);
> +		_exit(0);
> +	}
> +	h.pid = pid;
> +	h.go = go[1];
> +	ASSERT_EQ(read(ready[0], &c, 1), 1);
> +	ASSERT_EQ(map_both(pid, "1000 0 1\n"), 0);
> +	ASSERT_EQ(write(mapped[1], "g", 1), 1);
> +	ASSERT_EQ(read(ready[0], &c, 1), 1);
> +	EXPECT_EQ(in_child(r_v2_without_uid0_from_parent, &h), -EINVAL);
> +	reap_holder(h);
> +}

[ ... ]

> +static int idmap_case(const struct idmap_case *c, long *r)
> +{
> +	struct holder ns, im;
> +	int pid, p[2], st;
> +
> +	ns = spawn_mapped(c->ns_kind, 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) ? SETUP_ERROR : 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;
> +}

[Severity: Medium]
Can these test helpers hang in read() or waitpid() and leak pipe file
descriptors when a setup step fails?

Unlike in_child() and spawn_holder(), the helpers x_nonzero_root(),
r_nests_x(), dropped_writes_inherited_fd(), x_unshares_r_maps(),
fcap_v2_without_uid0_from_parent, and idmap_case() do not close the unused
pipe ends in the parent and child after fork(), nor do they close the
remaining pipe descriptors before returning:

1. Because the parent keeps the write ends (s1[1], res[1], p[1], ready[1])
open in its own file descriptor table, if the child exits early on a setup
failure (such as in restrict_root(), drop_setfcap_keep_bset(),
unshare(CLONE_NEWUSER), mount(), or new_file()), the parent's blocking
read() on s1[0], res[0], p[0], or ready[0] never sees EOF and blocks until
the test runner times out instead of returning SETUP_ERROR.

2. In r_nests_x() when x_maps is 0, if unshare(CLONE_NEWUSER) fails in the
child, the child sets r = SETUP_ERROR, skips writing to s1[1], writes r to
res[1], and exits:

	if (!pid) {
		restrict_root();
		if (unshare(CLONE_NEWUSER))
			r = SETUP_ERROR;
		else if (x_maps)
			r = write_map(0, "5 5 1\n");
		else if (write(s1[1], "u", 1) != 1 || read(s2[0], &c, 1) != 1)
			r = SETUP_ERROR;

Meanwhile the parent is blocked on read(s1[0], &c, 1) with s1[1] still open
in the parent process.

3. In x_nonzero_root(), if map_both(pid, "1000 0 1\n") fails in the parent,
short-circuit evaluation skips write(s2[1], "g", 1) and calls
waitpid(pid, NULL, 0) without killing the child or closing s2[1] (unlike
r_nests_x() and x_unshares_r_maps() which call kill(pid, SIGKILL) before
waitpid()), leaving waitpid() blocked while the child waits on
read(s2[0], &c, 1).

4. In fcap_v2_without_uid0_from_parent, the child keeps mapped[1] and go[1]
open, so if a parent ASSERT_EQ() fails and exits before reap_holder(), the
child stays blocked on read(mapped[0], &c, 1) or read(go[0], &c, 1).

Should these helpers close the unused pipe ends after fork(), close the
remaining pipe descriptors before returning, and ensure x_nonzero_root()
kills the child before waitpid() on error?

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

      reply	other threads:[~2026-10-26 21:22 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
2026-10-26 21:00 ` [PATCH v7 1/6] userns: record how far up CAP_SETFCAP reaches Josef Bacik
2026-10-26 21:00 ` [PATCH v7 2/6] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
2026-10-26 21:00 ` [PATCH v7 3/6] userns: check the opener and the writer before mapping uid 0 Josef Bacik
2026-10-26 21:00 ` [PATCH v7 4/6] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
2026-10-26 21:00 ` [PATCH v7 5/6] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
2026-10-26 21:00 ` [PATCH v7 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces Josef Bacik
2026-10-26 21:22   ` 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-185749@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