Skip to content

node: an administrator judges the service's own state as the service, and an unreadable state directory refuses once with its true reason - #1635

Merged
christianparpart merged 2 commits into
masterfrom
claude/raft-self-refusal-names-the-mode
Oct 6, 2026
Merged

christianparpart merged 2 commits into
masterfrom
claude/raft-self-refusal-names-the-mode

Conversation

@christianparpart

Copy link
Copy Markdown
Member

Found on this machine's own installation, while redeploying it to master 4d6255fca: an administrator judging the healthy, running node's own registered line with --print-surfaces was refused.

What was wrong

  • Measured with the installed 0.3.0-63-g4d6255fca, elevated, over the running service's state directory:
    • every state file is owned by NT SERVICE\FastCacheCompileNode;
    • the ownership check therefore refused to read them (neither the caller nor an administrative account), so the node read mode: none;
    • and it refused --raft-self with "the port comes from --listen-raft, which is also what turns consensus ON". But --listen-raft WAS on the line, and since Zero-config Windows office fleet #1600 the mode held in the state directory is what decides consensus.
  • Consequences:
    • a confident wrong signal to the operator;
    • every future redeploy's judgement of the live line would be refused the same way;
    • the deeper remedy text told the operator to remove files merely because the node's own service owns them, which on a running node throws away its identity.

What changes

  • One early refusal for an unreadable state directory, before any flag rule can blame --listen-raft, --discovery, --dashboard or the closed-consensus worker check. It names the true reason and a remedy that is true for that reason. It is refused by the step that failed:
    • a listing or access-list query that failed exits 1, which the supervisor retries as before;
    • a file another account owns, or an unknown entry, exits 78 (--print-surfaces: 2).
  • Owner ruling: an administrator judging, in a verb that writes nothing, counts files owned by the node's OWN per-service account (NT SERVICE\<--service-name>) as the node's own.
    • Only --print-surfaces trusts them. Which verbs write is a table column; --print-identity (which can mint), a foreground start and --install-service keep the strict owner set.
    • Only a per-service SID counts, compared as SIDs: --service-name="ALL SERVICES" (the group S-1-5-80-0) is refused, and an empty or unresolvable name trusts nothing.
    • --service-name comes from the command line only.
    • Stated blind spot: it fails OPEN only for an administrator who deliberately names ANOTHER service, and --print-surfaces writes nothing.
    • POSIX is unchanged, and the rule says what extending it would take.
  • No remedy tells an operator to remove something because the node's own service account owns it. Removal is offered only for a planted file.

Verification

  • WSL, whole binaries:

    • fastcache-compile-node-tests: 1452 cases, 1451 passed, 1 skipped;
    • FastCacheTest: 3218 cases, 3211 passed, 7 skipped.
  • Neuters: each of the six new guards was neutered in turn, and only its own cases went red.

  • Formatting and lint: the pinned clang-format and clang-tidy (22.1.8) are clean; a planted violation was caught. MSVC compiles the changed files clean with warnings as errors.

  • A Windows test runs the production SID code (NT SERVICE\TrustedInstaller and a test file's owner).

  • End to end on Windows, the branch built as cl-release 0.3.0-64-gd0bba3958, against the live node, whose service kept running:

    case before (0.3.0-63) after
    elevated --print-surfaces, live line mode: none, rc 2, blames --listen-raft mode: solitary, rc 0
    unelevated --print-surfaces, live line (not measured) rc 1, the true reason (cannot list the directory: access denied) and who can
    elevated FOREGROUND start over a copy of the service-owned state (owners and access lists kept) (not measured) refused, exit 78, nothing written, naming the owner and the safe remedy

…dministrator judges the service's own files only where nothing is written

The `--raft-self` refusal told an operator who had typed `--listen-raft` that its
absence was the cause. MEASURED cause: an elevated `--print-surfaces` over a healthy
node's state directory, whose files NT SERVICE\FastCacheCompileNode owns. The owner
judgement, made from the asking account, refused them, so no formation record shaped
the line. Its mode was unknown, and the first startup row that judges a flag by the
mode blamed the flag.

The cause is carried, not guessed:
- `ShapeByKeptFormation` is `main`'s shaping step. Where nothing can shape a
  configuration it records why: `NodeConfig::formationUnread`, a `FormationUnread`
  with the reading's own refusal and the start stage of the arm that gave up.
- `FormationAbsenceOf` is the ONE sentence, printed by the `mode: none` line and by
  the refusal.
- The raft line no longer promises a first-start mint when the record could not
  be read.

An unread directory is refused ONCE, and by its own arm:
- `main` ends a start on it FIRST (`UnreadStateStage`), ahead of the startup table.
  An I/O arm (a listing refused, an access list that would not answer, a record that
  would not decode) exits 1 and is retried by the supervisor, as before. A verdict
  (another account's file, an unknown entry) exits 78.
- `--print-surfaces` ends by the same arm, 1 against 2.
- The table's first row (`StateDirectoryUnreadRefusal`, a row whose message is a
  function: `ConfigRule::explain`) claims only the permanent ones. So `--raft-self`,
  typed `--discovery`, `--dashboard` and the closed-consensus worker never judge a
  mode the line does not have.
- `--raft-self`'s own refusal is two rows, never one sentence:
  - an empty `--listen-raft=` that closed the port its mode opens;
  - a configuration nothing tried to shape.

The trusted-owner set (owner ruling 2026-10-06, limited by the controller):
- An administrator counts files, and a directory, owned by the node's OWN per-service
  virtual account (`NT SERVICE\<--service-name>`, through the `NodeWindowsLogon` the
  registration uses) as the node's own. This holds ONLY in a verb that writes
  nothing.
- That is a column of the verbs (`StateUseRows`). `--print-surfaces` alone inspects.
  `--print-identity` mints an absent key and id, so it runs, as do a start and an
  install.
- The set is one row per (caller, use) pair (`OwnStateAccountRows`), applied by one
  decorator, `OwnStateAccountsGuard`. It promotes a file owner, and a directory owner
  through the new `WritersBeyondOwner`.
- The comparison is by SID (`FileOwner::id`, `AccountIdOf`), never by name, and only a
  per-service SID counts (`IsPerServiceSid`; `ALL SERVICES` is the group S-1-5-80-0).
- A start, foreground or supervised, keeps the strict set.
- POSIX is unchanged.

Remedies: a refusal of another account's file or directory names the owner and the
seat that would judge it as the service. Removing or moving aside is for a planted
entry only. A refused listing says who can list it, with one full stop.

Docs, the rule (with its blind spot: another service an administrator names on purpose
is trusted by `--print-surfaces`, failing OPEN for that administrator alone), the
reference YAML, the help text and an AGENT.md tripwire are updated, the last within
budget.

Signed-off-by: Christian Parpart <christian@parpart.family>
@christianparpart christianparpart added the type/bug Behaves incorrectly against its stated contract label Oct 6, 2026
@github-actions github-actions Bot added area/distributed Distributed/, apps/fastcache-compile-node/ - dispatch, workers, scheduler area/platform Platform/, Config/, Cli/ - service registration, config lookup, the CLI table area/packaging packaging/, CPack, deb/rpm/pkg/msi, installers labels Oct 6, 2026
The guard interface gained WritersBeyondOwner in the previous commit, and
the one test fake that implements it lives under `#if defined(_WIN32)`, so
no Linux build and no local gate compiled it; CI's clang-cl leg refused it
as an abstract class marked final. It now forwards to the production guard
like every other method it does not intercept.

Signed-off-by: Christian Parpart <christian@parpart.family>
@christianparpart
christianparpart merged commit 5c46835 into master Oct 6, 2026
58 of 59 checks passed
@christianparpart
christianparpart deleted the claude/raft-self-refusal-names-the-mode branch October 6, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/distributed Distributed/, apps/fastcache-compile-node/ - dispatch, workers, scheduler area/packaging packaging/, CPack, deb/rpm/pkg/msi, installers area/platform Platform/, Config/, Cli/ - service registration, config lookup, the CLI table type/bug Behaves incorrectly against its stated contract

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant