fix(perms): never write a never-seen player into Carbon's shared blank record
Some checks failed
PR Checks / plugin-checks (pull_request) Failing after -14s

Carbon 2.0.262 answers GetUserData for a Steam id it has no record for with
one shared, static blank record, and GrantUserPermission and AddUserGroup
write into it. A grant or membership for one never-seen player was then held
by every Steam id Carbon had no record for, until the server restarted, and
UserHasGroup read the membership back as a success, so the sync reported it
applied and later syncs saw nothing to fix.

On Carbon the sync now checks permission.UserExists before either write. A
never-seen player's grant is reported notLanded and their membership pending,
without touching the store; both land at the first audit sync after they
join, as PLAN.md section 29.8 and section 12.2 rule 4 already describe. Oxide
creates a real record and is unchanged.

Proven on the Carbon rig (Rust 26139947, Carbon 2.0.262): before, three
random never-seen ids each held autodoors.use after one admin grant; after,
a known id got its grant and membership, the never-seen id was reported
notLanded + pending, and random ids held nothing. Found in the account
deletion walk (docs/website/ACCOUNT_DELETION.md step 6, W3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-30 04:23:31 -05:00
parent 19ad9279fa
commit 59db88d006

View File

@@ -4778,6 +4778,15 @@ namespace Oxide.Plugins
break;
case PermOpKind.EnsureMember:
// Before the read, because on Carbon the read is the thing that lies: a
// never-seen player's UserHasGroup reads the shared blank record (see
// HasNoCarbonRecord), so it can say "already a member" of nobody.
if (HasNoCarbonRecord(op.Subject))
{
job.Pending.Add(op.Subject + ":" + op.Object);
break;
}
if (permission.UserHasGroup(op.Subject, op.Object))
{
job.AlreadyCorrect++;
@@ -4800,6 +4809,15 @@ namespace Oxide.Plugins
if (!Registered(job, op.Object))
break;
// Never-seen on Carbon: the grant would land in the shared blank record and be
// held by every Steam id Carbon has no record for. It lands at the first sync
// after they join, as PLAN.md §29.8 already says of Carbon.
if (HasNoCarbonRecord(op.Subject))
{
job.NotLanded.Add(op.Subject + ":" + op.Object);
break;
}
if (HoldsDirectly(op.Subject, op.Object))
{
job.AlreadyCorrect++;
@@ -4942,6 +4960,24 @@ namespace Oxide.Plugins
return false;
}
/// <summary>
/// Whether this is Carbon and Carbon has no user record for the player.
///
/// <para>
/// Carbon (2.0.262) answers <c>GetUserData</c> for a Steam id it has never seen with ONE
/// shared, static blank record, and <c>AddUserGroup</c> and <c>GrantUserPermission</c>
/// write into whatever it returns. So a membership or grant for one never-seen player
/// goes to that shared record and is then held by every Steam id Carbon has no record
/// for, until the server restarts — and <c>UserHasGroup</c> reads it back as a success.
/// Oxide creates a real record instead, so this is never true there and Oxide's
/// behaviour is unchanged. Found in the account deletion walk (W3).
/// </para>
/// </summary>
private bool HasNoCarbonRecord(string steamId)
{
return IsCarbon() && !permission.UserExists(steamId);
}
/// <summary>
/// Whether a player holds a permission through a grant of their own, as opposed to
/// through a group.