From 46c8796b3f0b1c641217a9255375f678fc688d50 Mon Sep 17 00:00:00 2001 From: Egor Ignatov Date: Tue, 28 Apr 2026 14:34:42 +0500 Subject: [PATCH 1/2] libtcb: add setgroups_allowed() helper Detect /proc/self/setgroups == "deny" to recognize an unprivileged user namespace where setgroups(2) is permanently denied by the kernel. No-op on non-Linux. --- ChangeLog | 9 +++++++++ libs/libtcb.c | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/ChangeLog b/ChangeLog index 001f011..e8a1b41 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2026-04-28 Egor Ignatov + + libtcb: add setgroups_allowed() helper. + Detect /proc/self/setgroups == "deny" to recognize an unprivileged + user namespace where setgroups(2) is permanently denied by the + kernel. No-op on non-Linux. + * libs/libtcb.c (setgroups_allowed) [__linux__]: New function. + (setgroups_allowed) [!__linux__]: New stub returning 1. + 2024-12-22 Björn Esser tcb_(un)convert: Check for UID and EUID to be 0 before proceeding. diff --git a/libs/libtcb.c b/libs/libtcb.c index 872e435..c61da15 100644 --- a/libs/libtcb.c +++ b/libs/libtcb.c @@ -167,6 +167,34 @@ static int sys_setgroups(size_t size, const gid_t *list) return syscall(SYS_setgroups, size, list); } +#ifdef __linux__ +/* + * In an unprivileged user namespace the kernel writes "deny" to + * /proc/self/setgroups, after which setgroups(2) is permanently + * denied for this process. Detect that situation so callers of + * tcb_drop_priv_r() can short-circuit instead of failing on + * setgroups(0, NULL). + */ +static int setgroups_allowed(void) +{ + int fd; + char buf[16]; + ssize_t n; + + fd = open("/proc/self/setgroups", O_RDONLY | O_NOCTTY); + if (fd == -1) + return 1; + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + return 1; + buf[n] = '\0'; + return strncmp(buf, "deny", 4) != 0; +} +#else +static int setgroups_allowed(void) { return 1; } +#endif + #define PRIV_MAGIC 0x1004000a #define PRIV_MAGIC_NONROOT 0xdead000a From 9f42bc3d9e9f498f3cad6f09147148127e5f52c6 Mon Sep 17 00:00:00 2001 From: Egor Ignatov Date: Tue, 28 Apr 2026 14:39:01 +0500 Subject: [PATCH 2/2] libtcb: skip privilege drop in unprivileged user namespaces When /proc/self/setgroups is "deny" we cannot call setgroups(2), and euid 0 inside such a namespace does not carry real privileges anyway. Treat this case the same as running non-root: mark the privs structure with PRIV_MAGIC_NONROOT and return success. Fixes failures of pam_tcb, libnss_tcb, tcb_unconvert and shadow's shadowtcb_drop_priv() when running under rootless container. --- ChangeLog | 10 ++++++++++ libs/libtcb.c | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/ChangeLog b/ChangeLog index e8a1b41..62e4f6a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,15 @@ 2026-04-28 Egor Ignatov + libtcb: skip privilege drop in unprivileged user namespaces. + When /proc/self/setgroups is "deny" we cannot call setgroups(2), + and euid 0 inside such a namespace does not carry real privileges + anyway. Treat this case the same as running non-root: mark the + privs structure with PRIV_MAGIC_NONROOT and return success. + Fixes failures of pam_tcb, libnss_tcb, tcb_unconvert and shadow's + shadowtcb_drop_priv() when running under rootless container. + * libs/libtcb.c (tcb_drop_priv_r): Behave as non-root when + setgroups_allowed() returns 0. + libtcb: add setgroups_allowed() helper. Detect /proc/self/setgroups == "deny" to recognize an unprivileged user namespace where setgroups(2) is permanently denied by the diff --git a/libs/libtcb.c b/libs/libtcb.c index c61da15..6eea24e 100644 --- a/libs/libtcb.c +++ b/libs/libtcb.c @@ -216,6 +216,17 @@ int tcb_drop_priv_r(const char *name, struct tcb_privs *p) return 0; } +/* + * In an unprivileged user namespace setgroups(2) is denied by the + * kernel. Without it we cannot drop supplementary groups, and being + * euid 0 inside such a namespace does not carry real privileges + * anyway, so behave as if we were non-root. + */ + if (!setgroups_allowed()) { + p->is_dropped = PRIV_MAGIC_NONROOT; + return 0; + } + if (stat(TCB_DIR, &st)) return -1;