Skip to content

Enforce ownership check on shmctl(IPC_RMID) - #14401

Open
adi-IL wants to merge 2 commits into
google:masterfrom
adi-IL:fix/shm-ipcrmid-own-check
Open

Enforce ownership check on shmctl(IPC_RMID)#14401
adi-IL wants to merge 2 commits into
google:masterfrom
adi-IL:fix/shm-ipcrmid-own-check

Conversation

@adi-IL

@adi-IL adi-IL commented Aug 25, 2026

Copy link
Copy Markdown

Fixes #14191

What was wrong

Any uid could destroy any SysV shared memory segment via shmctl(IPC_RMID). Shmctl in pkg/sentry/syscalls/linux/sys_shm.go directly invoked segment.MarkDestroyed(t) without checking that the caller is the creator, the current owner, or holds CAP_SYS_ADMIN in the owning user namespace. In Linux (ipc/shm.c:shmctl_down), IPC_RMID and IPC_SET both require ipcctl_obtain_check. In gVisor, IPC_SET already enforces this via s.obj.Set, and other SysV mechanisms (semaphore, msgqueue) enforce it via Registry.Remove, but shmctl(IPC_RMID) was unauthenticated.

What changed

  1. Authorization in Shm.MarkDestroyed (pkg/sentry/kernel/shm/shm.go):

    • Shm.MarkDestroyed(ctx context.Context) error now extracts credentials via auth.CredentialsFromContext(ctx) and executes s.obj.CheckOwnership(creds) under s.mu.Lock() within the same critical section that sets pendingDestruction = true.
    • Returns linuxerr.EPERM if unauthorized.
    • Prevents data races and TOCTOU with concurrent shmctl(IPC_SET) calls while keeping the ownership policy centralized in the shm package.
    • Calls s.registry.dissociateKey(s) and s.DecRef(ctx) outside s.mu to preserve the Registry.mu -> Shm.mu lock hierarchy.
  2. Syscall Handler (pkg/sentry/syscalls/linux/sys_shm.go):

    • In Shmctl, linux.IPC_RMID directly delegates to return 0, nil, segment.MarkDestroyed(t).
  3. Syscall Regression Test (test/syscalls/linux/shm.cc):

    • Added ShmTest.RmidOwnershipPermissionDenied.
    • Verifies that a non-owner/non-creator process with dropped privileges (CAP_SYS_ADMIN dropped, scratch UID/GID) receives EPERM when calling shmctl(IPC_RMID).
    • Confirms the segment survives and SHM_DEST is not set on IPC_STAT.
    • Verifies the positive path: after transferring ownership to the scratch user via IPC_SET, shmctl(IPC_RMID) succeeds.

Verification

  • make test TARGETS="//test/syscalls:shm_test_runsc_ptrace": PASS
  • make test TARGETS="//test/syscalls:shm_test_native": PASS

@ayushr2

ayushr2 commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Please add a syscall test. The issue gave a nice reproducer. Make sure that the newly added syscall test covers the reproducer, fails without your fix and passes with your fix.

@adi-IL

adi-IL commented Aug 25, 2026

Copy link
Copy Markdown
Author

Added the syscall test ShmTest.RmidOwnershipPermissionDenied in test/syscalls/linux/shm.cc and updated dependencies in test/syscalls/linux/BUILD.

The test creates a 0600 SysV shared memory segment as the creator, drops CAP_SYS_ADMIN and switches credentials to a scratch UID/GID in a scoped thread, and confirms that shmctl(id, IPC_RMID, nullptr) returns EPERM and does not destroy the segment.

Verification:

  • Without the fix, make test TARGETS="//test/syscalls:shm_test_runsc_ptrace" failed as expected:
[ RUN      ] ShmTest.RmidOwnershipPermissionDenied
test/syscalls/linux/shm.cc:568: Failure
Value of: Shmctl<void>(shm.id(), 0, nullptr)
Expected: has an errno value that is equal to 1, and has an error message that is anything
  Actual: No Error, has a value 0
  • With the fix, both //test/syscalls:shm_test_runsc_ptrace and //test/syscalls:shm_test_native pass.

Comment thread pkg/sentry/syscalls/linux/sys_shm.go Outdated

@ayushr2 ayushr2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please squash your commits

@adi-IL

adi-IL commented Aug 25, 2026

Copy link
Copy Markdown
Author

Thanks @ayushr2, that makes complete sense.

Updated the implementation:

  1. Moved the CheckOwnership check into Shm.MarkDestroyed in pkg/sentry/kernel/shm/shm.go under s.mu.Lock(), returning linuxerr.EPERM if unauthorized. This prevents TOCTOU and data races against concurrent shmctl(IPC_SET) calls and centralizes the ownership policy inside the shm package.
  2. In pkg/sentry/syscalls/linux/sys_shm.go, IPC_RMID now directly forwards to segment.MarkDestroyed(t).
  3. Squashed all commits into a single commit on the branch.

Verified that //test/syscalls:shm_test_runsc_ptrace and //test/syscalls:shm_test_native pass cleanly.

Comment thread pkg/sentry/kernel/shm/shm.go Outdated

@ayushr2 ayushr2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix — the bug is real and the scope is right. ipcctl_obtain_check() is only reached from shmctl_down() in Linux (ipc/shm.c), which handles exactly IPC_RMID and IPC_SET, so IPC_RMID was the only missing case: IPC_SET already goes through ipc.Object.Set, IPC_STAT/SHM_STAT correctly check read permission in Shm.IPCStat instead, and IPC_INFO/SHM_INFO have no check in Linux either. (SHM_LOCK/SHM_UNLOCK does skip its CAP_IPC_LOCK-or-euid check, but that's a different predicate on a no-op path — separate issue, not this PR.)

Moving the check into the shm package under s.mu is the right call. A few things left, one substantive — see inline comments.

On the commit message: please use a plain imperative title per repo style, e.g. "Enforce ownership check on shmctl(IPC_RMID)" without the fix: prefix, and drop the Assisted-by: muse spark 1.2 trailer. The PR description is also stale — it still describes the first approach ("In pkg/sentry/syscalls/linux/sys_shm.go:147, obtain creds via auth.CredentialsFromContext(t)...") along with build-process narration that doesn't belong in permanent history.

Comment thread pkg/sentry/kernel/shm/shm.go Outdated
Comment thread pkg/sentry/kernel/shm/shm.go Outdated
Comment thread test/syscalls/linux/shm.cc
@ayushr2

ayushr2 commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

AI usage disclaimer: the above review is from Claude

@adi-IL
adi-IL force-pushed the fix/shm-ipcrmid-own-check branch from 5b7178b to e0edfc4 Compare August 25, 2026 22:58
@adi-IL adi-IL changed the title fix: enforce ownership check on shmctl IPC_RMID Enforce ownership check on shmctl(IPC_RMID) Aug 25, 2026
@adi-IL

adi-IL commented Aug 25, 2026

Copy link
Copy Markdown
Author

Thanks @ayushr2, all feedback addressed:

  1. Atomic Check and Mark: Folded CheckOwnership and pendingDestruction = true into a single critical section under s.mu in pkg/sentry/kernel/shm/shm.go, eliminating the lock bounce and making the authorization atomic with the state transition. Key dissociation and dereferencing remain outside s.mu to preserve the registry-to-shm lock hierarchy.
  2. Docstring: Updated Shm.MarkDestroyed docstring to document the permission contract, returned error codes, and credential requirement.
  3. Test Coverage: Updated ShmTest.RmidOwnershipPermissionDenied in test/syscalls/linux/shm.cc to assert EXPECT_FALSE(attr.shm_perm.mode & SHM_DEST) and added the positive path verifying that shmctl(IPC_RMID) succeeds after ownership is transferred to the scratch user via IPC_SET.
  4. Commit & PR Cleanup: Squashed into a single commit with plain imperative title Enforce ownership check on shmctl(IPC_RMID), removed the AI trailer, and updated the PR description.

Both //test/syscalls:shm_test_runsc_ptrace and //test/syscalls:shm_test_native pass cleanly.

Comment thread pkg/sentry/kernel/shm/shm.go Outdated
Any uid could destroy any SysV shared memory segment via
shmctl(IPC_RMID) because Shmctl bypassed the ownership check that
Registry.Remove enforces for semaphore and msgqueue. Only the creator
or owner or a task with CAP_SYS_ADMIN in the owning user namespace
should be allowed to mark a segment for destruction, matching
Linux ipcctl_obtain_check and object CheckOwnership.

Enforce CheckOwnership inside Shm.MarkDestroyed under s.mu.Lock() in the
same critical section that transitions pendingDestruction to true,
returning linuxerr.EPERM if unauthorized. This ensures atomic authorization
and state transition without lock bouncing, and prevents TOCTOU against
concurrent shmctl(IPC_SET). Key dissociation occurs outside s.mu to
preserve the registry-to-shm lock hierarchy.

Add a syscall regression test RmidOwnershipPermissionDenied in
test/syscalls/linux/shm.cc covering permission denial on non-owners,
SHM_DEST survival check, and successful destruction following ownership
transfer via IPC_SET.

Fixes google#14191
@adi-IL
adi-IL force-pushed the fix/shm-ipcrmid-own-check branch from e0edfc4 to ef54363 Compare August 26, 2026 10:04

@ayushr2 ayushr2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Claude here, commenting on behalf of @ayushr2.)

All three substantive points from the previous round are addressed, and the dissociateKey reorder checks out: Registry.remove — the one place that panics on "key still associated" — only runs from the refcount-zero destructor in Shm.DecRef, and the self-reference isn't dropped until the end of MarkDestroyed, after dissociation. The early-return path never DecRefs, so the panic precondition can't be hit. Check and transition are now atomic, the doc comment states the permission contract, and gofmt is clean.

Only nits left, inline. Note the C++ couldn't be verified beyond reading (no clang-format available in my environment), and go build doesn't work in a plain checkout since the package depends on bazel-generated sources — though the Go change only adds an error return, with both auth and linuxerr already imported.

Comment thread pkg/sentry/kernel/shm/shm.go Outdated
Comment thread test/syscalls/linux/shm.cc Outdated
Comment thread test/syscalls/linux/shm.cc Outdated
attr.shm_perm.uid = scratch_uid;
ASSERT_NO_ERRNO(Shmctl(shm.id(), IPC_SET, &attr));

// In the same dropped-privilege thread (now the owner), IPC_RMID should succeed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 83 columns.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped in 448a3cb.

Comment thread test/syscalls/linux/shm.cc Outdated
ShmSegment::Rmid already releases the id only after IPC_RMID
succeeds, so the destructor remains as fallback if the
owner-path call fails. Dedup the dropped-privilege thread
bodies behind a shared helper and wrap comments to 80 columns.
@adi-IL

adi-IL commented Aug 26, 2026

Copy link
Copy Markdown
Author

@ayushr2 I want a decision on the MarkDestroyed docstring so we do not reverse it again.

gVisor CONTRIBUTING.md allows AI. Labeling Claude was the right call. It also says the person posting has to review what the model wrote before it lands on the thread. I think that is what slipped here.

#14401 (comment) asked us to document the ownership contract and the EPERM return, because that is now part of the API.

#14401 (comment) asked us to revert that and not describe what the function is doing. We reverted in ef54363.

The latest review still says the doc comment states the permission contract. That text is not in the tree. The comment is the original upstream wording, rewrapped in 448a3cb.

I will put the contract back, or I will leave the short original. I need you to pick one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Any uid can destroy any SysV shared memory segment

2 participants