Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
2026-05-28 Egor Ignatov <egori at altlinux.org>

libtcb: tolerate setgroups EPERM in unprivileged user namespaces.
In a user namespace where /proc/self/setgroups is "deny",
setgroups(2) is permanently rejected by the kernel. Perform the
regular privilege drop and only tolerate sys_setgroups(0, NULL)
failing with EPERM in such a namespace; in that case the kernel
guarantees no supplementary group could have been gained via the
namespace, so leaving the list in place is safe. Record this
with a new PRIV_MAGIC_NOSETGROUPS state so that tcb_gain_priv_r()
skips the matching setgroups() call.
Fixes failures of pam_tcb, libnss_tcb, tcb_unconvert and shadow's
shadowtcb_drop_priv() when running under rootless container.
* libs/libtcb.c (PRIV_MAGIC_NOSETGROUPS): New magic value.
(tcb_drop_priv_r): Tolerate EPERM from sys_setgroups(0, NULL)
when setgroups_allowed() returns 0 and record this by setting
p->is_dropped to PRIV_MAGIC_NOSETGROUPS.
(tcb_gain_priv_r): Accept PRIV_MAGIC_NOSETGROUPS and skip
sys_setgroups() in that state.

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.
Comment thread
Blarse marked this conversation as resolved.

libtcb: change the type of is_dropped to unsigned int.
The PRIV_MAGIC_* values used to mark the privilege state do not
fit into a signed int, so store them in an unsigned field to avoid
a signedness mismatch.
* include/tcb.h (struct tcb_privs): Change the type of the
is_dropped field to unsigned int.

2024-12-22 Björn Esser <besser82 at fedoraproject.org>

tcb_(un)convert: Check for UID and EUID to be 0 before proceeding.
Expand Down
2 changes: 1 addition & 1 deletion include/tcb.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct tcb_privs {
int number_of_groups;
gid_t old_gid;
uid_t old_uid;
int is_dropped;
unsigned int is_dropped;
};

extern int lckpwdf_tcb(const char *);
Expand Down
55 changes: 51 additions & 4 deletions libs/libtcb.c
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,41 @@ static int sys_setgroups(size_t size, const gid_t *list)
return syscall(SYS_setgroups, size, list);
}

/*
* In an unprivileged user namespace the linux kernel writes "deny" to
* /proc/self/setgroups, after which setgroups(2) is permanently
* denied for this process. Used to disambiguate an EPERM from
* setgroups(0, NULL): if this returns 0, the failure is the
* kernel's permanent denial rather than a real error.
*/
static int setgroups_allowed(void)
{
#ifdef __linux__
int fd;
char buf[5];
ssize_t n;

fd = open("/proc/self/setgroups", O_RDONLY | O_NOCTTY);
if (fd == -1)
return 1;
n = read(fd, buf, 5);
close(fd);
if (n != 5)
return 1;
return memcmp(buf, "deny\n", 5) != 0;
#else
return 1;
#endif
}

#define PRIV_MAGIC 0x1004000a
#define PRIV_MAGIC_NONROOT 0xdead000a
#define PRIV_MAGIC_NOSETGROUPS 0xbeef000a

int tcb_drop_priv_r(const char *name, struct tcb_privs *p)
{
int res;
unsigned int magic = PRIV_MAGIC;
struct stat st;
gid_t shadow_gid = -1;
char *dir;
Expand Down Expand Up @@ -207,14 +236,30 @@ int tcb_drop_priv_r(const char *name, struct tcb_privs *p)

p->number_of_groups = res;

if (sys_setgroups(0, NULL) == -1)
return -1;
/*
* Try to clear the supplementary group list. In a user namespace
* where /proc/self/setgroups is "deny", setgroups(2) is permanently
* denied and the call returns EPERM. In that case the kernel
* guarantees the caller did not gain any group via the namespace
* mechanism, so it is safe to leave the list in place; we record
* this via PRIV_MAGIC_NOSETGROUPS so tcb_gain_priv_r() skips the
* matching setgroups() call.
*/
if (sys_setgroups(0, NULL) == -1) {
int saved_errno = errno;
if (errno != EPERM || setgroups_allowed()) {
errno = saved_errno;
return -1;
}
magic = PRIV_MAGIC_NOSETGROUPS;
}

if (!ch_gid(shadow_gid, &p->old_gid))
return -1;
if (!ch_uid(st.st_uid, &p->old_uid))
return -1;

p->is_dropped = PRIV_MAGIC;
p->is_dropped = magic;
return 0;
}

Expand All @@ -226,6 +271,7 @@ int tcb_gain_priv_r(struct tcb_privs *p)
return 0;

case PRIV_MAGIC:
case PRIV_MAGIC_NOSETGROUPS:
break;

default:
Expand All @@ -237,7 +283,8 @@ int tcb_gain_priv_r(struct tcb_privs *p)
return -1;
if (!ch_gid(p->old_gid, NULL))
return -1;
if (sys_setgroups(p->number_of_groups, p->grplist) == -1)
if (p->is_dropped != PRIV_MAGIC_NOSETGROUPS &&
sys_setgroups(p->number_of_groups, p->grplist) == -1)
return -1;

p->is_dropped = 0;
Expand Down