All of lore.kernel.org
 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: 13+ 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:07   ` sashiko-bot
2026-10-26 21:00 ` [PATCH v7 2/6] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
2026-10-26 21:18   ` sashiko-bot
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:36   ` sashiko-bot
2026-10-26 21:00 ` [PATCH v7 4/6] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
2026-10-26 21:28   ` sashiko-bot
2026-10-26 21:00 ` [PATCH v7 5/6] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
2026-10-26 21:20   ` sashiko-bot
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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.