Carbon: a direct grant to a never-seen Steam id leaks onto every never-seen id #32

Open
opened 2026-10-12 18:18:05 +00:00 by wtclaude · 0 comments
Member

Found during the PLAN_REDESIGNS §11.3 Backpacks spike (2026-10-12) on rust-carbon (Carbon 2.0.262, Rust 25877464). Oxide 2.0.7826 on the same Rust build behaves differently.

What happens

On Carbon, permission.GrantUserPermission(id, perm, null) for a Steam id the store has no user record for (a player who has never connected to that server) does not create a record for that id. The grant goes into a record that every unknown id shares. After that, every never-seen id reports holding the permission:

rgb.who 77   (76561199910000077: never used anywhere)
  Oxide : groups=[] own=[]                                        UserHasPermission(backpacks.use)=False
  Carbon: groups=[] own=[backpacks.use,backpacks.size.profile.6-48] UserHasPermission(backpacks.use)=True

Those two permissions had been granted to other never-seen ids (stand-ins 51 and 21). Revoking them from one unknown id cleared them for all of them (rgb.cleanup → id 77 then held []).

Oxide creates a real record for the id instead (GetUserData makes one), and nothing leaks.

Why it matters to the bridge

perm.sync's EnsureGrant (overlay/oxide/plugins/RunicGateway.cs ~4710) grants directly to a Steam id, and the site (D160) can push grants for linked players who have never joined that server: kit entitlements (rust_perm_run_grants), and soon the §11.3 backpack size lease. The comment at ~3539 says a direct grant "would have reached them immediately". That holds on Oxide. On Carbon, as far as I can tell:

  • the read-back HoldsDirectly uses GetPermissionUsers(perm), which probably does not list the id, so the op is reported notLanded (honest, but a re-push repeats it forever);
  • meanwhile every never-seen id answers UserHasPermission(perm) == true for anything granted this way. Any plugin that checks a permission for a player before their record exists (first-connect hooks) would see a grant that isn't theirs;
  • when the player does connect, Carbon makes them a fresh record, so the grant meant for them is not there until the next sync.

Not yet checked: whether GetPermissionUsers lists the id on Carbon, whether AddUserGroup leaks the same way (the bridge already treats a never-seen member as pending), and what a first connection does with the shared record.

Repro

Spike plugin RgBackpackSpike.cs (scratch, never shipped): rgb.perm <i> <perm> grants to 76561199910000000+i with no player; rgb.who <i> prints GetUserGroups, GetUserPermissions and UserHasPermission. Grant to one unused id, then who another unused id.

Likely fix direction (not decided): treat a direct grant to an id the Carbon store has no record for like a group membership: report it pending and land it on the first connection. Or check permission.UserExists(id) first on both frameworks.

Left for later, at the org lead's call (2026-10-12). Kept out of §11.3.


AI-assisted: found and written up by Claude Code (Claude Opus 5.5).

Found during the PLAN_REDESIGNS §11.3 Backpacks spike (2026-10-12) on `rust-carbon` (Carbon 2.0.262, Rust 25877464). Oxide 2.0.7826 on the same Rust build behaves differently. ## What happens On **Carbon**, `permission.GrantUserPermission(id, perm, null)` for a Steam id the store has **no user record for** (a player who has never connected to that server) does not create a record for that id. The grant goes into a record that every unknown id shares. After that, **every** never-seen id reports holding the permission: ``` rgb.who 77 (76561199910000077: never used anywhere) Oxide : groups=[] own=[] UserHasPermission(backpacks.use)=False Carbon: groups=[] own=[backpacks.use,backpacks.size.profile.6-48] UserHasPermission(backpacks.use)=True ``` Those two permissions had been granted to *other* never-seen ids (stand-ins 51 and 21). Revoking them from one unknown id cleared them for all of them (`rgb.cleanup` → id 77 then held `[]`). Oxide creates a real record for the id instead (`GetUserData` makes one), and nothing leaks. ## Why it matters to the bridge `perm.sync`'s `EnsureGrant` (`overlay/oxide/plugins/RunicGateway.cs` ~4710) grants **directly to a Steam id**, and the site (D160) can push grants for linked players who have never joined *that* server: kit entitlements (`rust_perm_run_grants`), and soon the §11.3 backpack size lease. The comment at ~3539 says a direct grant "would have reached them immediately". That holds on Oxide. On Carbon, as far as I can tell: - the read-back `HoldsDirectly` uses `GetPermissionUsers(perm)`, which probably does not list the id, so the op is reported `notLanded` (honest, but a re-push repeats it forever); - meanwhile **every** never-seen id answers `UserHasPermission(perm) == true` for anything granted this way. Any plugin that checks a permission for a player before their record exists (first-connect hooks) would see a grant that isn't theirs; - when the player does connect, Carbon makes them a fresh record, so the grant meant for them is not there until the next sync. Not yet checked: whether `GetPermissionUsers` lists the id on Carbon, whether `AddUserGroup` leaks the same way (the bridge already treats a never-seen member as `pending`), and what a first connection does with the shared record. ## Repro Spike plugin `RgBackpackSpike.cs` (scratch, never shipped): `rgb.perm <i> <perm>` grants to `76561199910000000+i` with no player; `rgb.who <i>` prints `GetUserGroups`, `GetUserPermissions` and `UserHasPermission`. Grant to one unused id, then `who` another unused id. Likely fix direction (not decided): treat a direct grant to an id the Carbon store has no record for like a group membership: report it `pending` and land it on the first connection. Or check `permission.UserExists(id)` first on both frameworks. Left for later, at the org lead's call (2026-10-12). Kept out of §11.3. --- AI-assisted: found and written up by Claude Code (Claude Opus 5.5).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Rust-Plugins#32
No description provided.