internal/as_user: drop supplementary groups - #2301
Conversation
set_eids() switched the effective gid and uid to the target user but never touched the supplementary group list, so the privilege-dropped thread kept the caller's group memberships while acting as that user. A directory reachable only through one of root's supplementary groups stayed reachable for the duration of the switch. Drop the list with setgroups(0, NULL) before relinquishing the gid and uid, while the thread still holds the privilege required to make that call, following the revocation order described in CERT POS36-C. Fixes coreos#2242 Signed-off-by: Jason Colapietro <jasoncola1@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📜 Recent review details
|
| Layer / File(s) | Summary |
|---|---|
Clear supplementary groups during credential changes internal/as_user/as_user.c, docs/release-notes.md |
set_eids calls setgroups(0, NULL) before changing credentials and returns -1 if the call fails. Its documentation and the release notes describe the behavior. |
Estimated code review effort: 2 (Simple) | ~10 minutes
Merge Risk: ⚪ Minimal · up to f0dc2
The change clears supplementary groups before dropping user and group privileges, preventing retained group memberships from granting unintended access. No actionable merge-blocking risk remains after normal checks and review.
Suggested reviewers: prestist
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | The title follows the required format and accurately describes dropping supplementary groups in internal/as_user. |
| Description check | ✅ Passed | The description clearly explains the supplementary-group bug, the fix, its impact, and validation results. |
| Linked Issues check | ✅ Passed | The changes satisfy issue #2242 by clearing supplementary groups before changing the effective GID and UID and returning errors on failure. |
| Out of Scope Changes check | ✅ Passed | The code and release-note changes directly support issue #2242 and contain no unrelated scope. |
| Docstring Coverage | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (1 skipped: 1 unsupported.) |
| Commit Message Convention | ✅ Passed | The PR has one non-merge commit: internal/as_user: drop supplementary groups; its subsystem is a path prefix, drop is lowercase imperative, and there is no trailing period. |
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
- Create stacked PR
- Commit on current branch
🧪 Generate unit tests (beta)
- Create PR with unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
Summary
set_eids()ininternal/as_user/as_user.cswitches the effective gid and uidto the target user, but never touches the supplementary group list. The
privilege-dropped thread therefore keeps the caller's group memberships — in
practice root's — while acting as that user.
This drops the list with
setgroups(0, NULL)before the gid and uid arerelinquished, while the thread still holds the privilege required to make that
call, following the revocation order described in
CERT POS36-C.
Reproducer
I built a probe against
as_user.cbefore and after the change. It makes thecaller (root) a member of gid 4242, creates a directory owned
root:4242withmode
0770— so "other" has no access — and then asksau_open()to create afile inside it as
uid=65534 gid=65534, which is not a member of 4242:au_opensucceeded (fd=3): the thread kept root's membership of gid 4242au_openfailed withEACCES, as it shouldSo the retained groups do grant real access during the switch, not just a
theoretical capability.
Impact
Low, and I don't want to oversell it. As #2242 notes, Ignition already runs as
root and
as_useris defense-in-depth rather than a security boundary; the onlycaller is
writeAuthKeysFile()ininternal/exec/util/passwd.go, writing SSHauthorized keys during first boot, where root's supplementary groups are
typically just
{0}. This is a correctness fix that brings the privilege dropin line with POSIX practice, and matters more for any future reuse of this code.
Notes
<grp.h>is added for thesetgroups()declaration; the cgo build uses-Werror=implicit-function-declaration, so a missing declaration would befatal rather than silent.
setgroups()fails,set_eids()returns an errorrather than continuing with a partial privilege drop.
setgroups(0, NULL)clears the list rather than installing the target user'sown groups. Installing the real list would mean either an NSS lookup inside
the cloned thread or widening
au_ids_tto carry the group list from Go.Happy to do the latter if you'd prefer it — clearing seemed like the right
minimal fix for the reported issue.
Testing
./test— Success, exit 0 (Fedora 44, Go 1.24, cgo, run as a non-rootuser)
./build ignition— exit 0git diff --check— cleanas_user.cRunning
./testas root instead failsTestTranslateTree/translate_7in theseven
butane/base/*packages, because that test asserts a permission-deniederror which root bypasses. That failure reproduces identically on an unmodified
origin/main, so it is unrelated to this change.No new in-tree test:
internal/as_useris Linux-, cgo- and root-only, has noexisting unit tests, and a regression test for this would have to manipulate the
test process's own credentials. I'm glad to add a root-gated one if you want it.
Fixes #2242.