Skip to content

Spend 0030's password on one writer, and refuse an answer with a fact missing - #260

Merged
iderex merged 1 commit into
mainfrom
the-passwords-life-and-the-facts-a-session-needs-30
Aug 31, 2026
Merged

Spend 0030's password on one writer, and refuse an answer with a fact missing#260
iderex merged 1 commit into
mainfrom
the-passwords-life-and-the-facts-a-session-needs-30

Conversation

@iderex

@iderex iderex commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

The issue this belongs to

#30. This does not close it, and the reason is at the end.

What changed

src/session/password.rs holds the part of 0030 a type and a closure over three
values settle.

The password is a value with no accessor, no copy and no printable form.
Password::present takes it by move and hands a borrow to one writer, so the
three shapes 0030 names as the ones that keep a credential alive past its
request - a credentials object held so a retry is easy, a field on the session so
a renewal can re-authenticate, a parameter threaded through a helper so a test
can sign in twice - do not compile. It performs no scrub and claims none: 0030
takes a plain string and states what the runtime leaves behind as a real cost and
an unmeasurable one, and the module states the same thing in the place a reader
would look for a promise.

The account name is the opposite case beside it, kept exactly as it was typed,
for the reason 0028 refuses to repair an address.

what_the_answer_yields is 0004's closure rule at this door. A body that parsed
and omitted the token, the account identifier or the validity statement is
answer-not-understood at the answer body expecting a field the core needs, and
the success side is a type whose three members are all present, so a branch that
wanted to return a session with a hole in it has nothing to return it in. A value
that arrived empty is read as one that did not arrive; that is this module's
reading rather than a sentence in 0030 and it is written as one, with the
argument for it, on the function that makes it.

Failure::from_status_with_no_token_presented is the third thing 0030 decides,
and it is in src/failure/mod.rs rather than beside the route because 0037 fixes
one mapping point for every value of the vocabulary. It moves the 401 payload
alone and is from_status unchanged for every other row.

No field name of any server appears anywhere in the change. What a body calls
these three is read where the body is read, which is #27, and a name written here
today would be a claim about an interface nobody has read - the same reason
tests/fake_server/surface.rs gives for the bodies it answers with.

What failure it prevents

A wrong payload three calls away from where it was produced. from_status mapped
every 401 as a token presented and rejected, because the status does not carry
the difference and no caller existed that knew it. 0034 reads that payload as a
session that has ended and starts a renewal; the same payload out of a sign-in
would answer a mistyped password with a renewal for a session that was never
established, and not-authenticated with no token presented is exactly the
distinction #34 and #35 branch on.

Beside it, the lifetime. 0030's own argument is that none of the three shapes
looks wrong in review, because the wrongness is the lifetime rather than the line
and a lifetime is not visible at a call site. This is that argument taken to the
type, so the first sign-in call written against #27 cannot take any of the three
without the build refusing it.

This is a failure that could happen rather than one that has: there is no sign-in
call in this tree to have got it wrong yet, which is the point of writing the
constraint before the caller.

Evidence

Every command run at 3c983246029d4cf4f54399c88ef1d70d08b7052e, the head being
pushed.

cargo build --locked --all-targets
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s

cargo test --locked 2>&1 | grep -c '^test .* ok$'
592

cargo test --locked 2>&1 | grep -c '^test result: FAILED'
0

cargo test --locked 2>&1 | grep -c '^test session::password::tests::'
11

The legs of the gate this machine can run, each by its own script and each by its
own exit code:

for s in format invariants doc-paths lint test excluded-targets fixture-scrub targets; do
  bash .github/$s/$s.sh check >/dev/null 2>&1; echo "$s exit=$?"
done
format exit=0
invariants exit=0
doc-paths exit=0
lint exit=0
test exit=0
excluded-targets exit=0
fixture-scrub exit=0
targets exit=0

The analyser was also run directly, with the groups the lint check denies:

cargo clippy --all-targets -- -D warnings -D clippy::all -D clippy::pedantic -D clippy::cargo 2>&1 | grep -cE '^(error|warning)'
0

What a guard here refuses, and the proof it bites

The password cannot be read twice, and the near miss is a compile failure
rather than a red line.
That is stated plainly rather than dressed as a test
result: Password is neither Copy nor Clone and present takes self, so
the violation reddens the build and not a case. The violation and the run:

(in a_password_is_readable_once_and_the_reading_consumes_it, a second line)
let written = password.present(str::to_owned);
password.present(str::to_owned);

cargo test --locked 2>&1 | grep -m1 -A5 'E0382'
error[E0382]: use of moved value: `password`
   --> src\session\password.rs:408:9
    |
406 |         let password = Password::supplied(String::from("hunter2"));
    |             -------- move occurs because `password` has type `Password`, which does not implement the `Copy` trait
407 |         let written = password.present(str::to_owned);

Without that line the same run is the green one pasted above.

The 401 payload at this door is the other one, and the near miss is the door
that reports a refused credential as a token presented.
Putting the default
back is the deliberate violation:

(in Failure::from_status_with_no_token_presented)
-                a_token_was_presented: false,
+                a_token_was_presented: true,

cargo test --locked --lib failure::tests::the_door_with_no_token 2>&1 | grep -E 'stdout|test result'
---- failure::tests::the_door_with_no_token_takes_the_other_payload_and_moves_no_other_row stdout ----
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 440 filtered out; finished in 0.00s

(with the line restored)
cargo test --locked --lib failure::tests::the_door_with_no_token 2>&1 | grep -E '^test result'
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 440 filtered out; finished in 0.00s

An answer missing one of the three yields no session. Taking the token check
out is the violation:

(in what_the_answer_yields)
-    let token = carried(read.token).ok_or_else(|| absent(FactNotCarried::Token))?;
+    let token = read.token.unwrap_or("");

cargo test --locked --lib session::password 2>&1 | grep -E '^    session|^test result'
    session::password::tests::a_body_missing_two_fields_reports_the_first_of_them
    session::password::tests::a_body_that_omits_one_of_the_three_is_an_answer_the_core_cannot_read
    session::password::tests::a_field_that_arrived_empty_is_read_as_one_that_did_not_arrive
test result: FAILED. 8 passed; 3 failed; 0 ignored; 0 measured; 430 filtered out; finished in 0.00s

(with the line restored)
cargo test --locked --lib session::password 2>&1 | grep -E '^test result'
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 430 filtered out; finished in 0.00s

The three that go red are the three that ask the question. Each violation above
was applied to the working tree, run, and reverted; the tree pushed is the one
the green runs at the top of this section were taken from.

What this does not cover

#30's own condition is untouched and this does not close it. The condition is
a sign-in against the fake server in #21 with each of three failures a distinct
named kind, and the transport that would carry one is #27, which is not built.
Nothing here sends or receives a byte. The reading already on the issue about the
grep half of that condition is not restated in this body.

No scrub, and no claim of one. Nothing here erases the bytes of a password.
What a heap page, a core dump or a suspended process holds afterwards is outside
everything in this change, and 0030's own alternatives section is where that
residual was accepted.

The coverage leg was not judged on this machine, and what it reports is not
this change's.
It reports the same on an unmodified checkout of the default
branch:

git stash -u && bash .github/coverage/coverage.sh check 2>&1 | tail -2
room   0 more uncovered line(s) on this surface before the bar is crossed
::error::Line coverage on the pinned surface is -%, under the bar of 96.5%.

The instrumentation reads zero lines for every area here, src/ included, so the
number is absent rather than low, and it is absent before the change as well as
after it. .github/workflows/coverage.yml runs on this pull request and is what
judges it. The pinned-surface register carries the new module with its reason, so
the surface is declared rather than left for a run to find missing.

Three other legs were not run here. headless needs a program this machine
does not carry (ip: command not found), thread-detector needs a nightly
compiler this tree deliberately does not pin, and mutation is weekly rather
than a pull-request leg. None of the three was skipped by editing anything; they
were not run, and the workflows are what run them.

Nothing was run against a real server, and nothing in this change has a real
server in its subject.

Who has read it

Nobody but me has read this change. There is no second reader on this board
tonight, and the evidence above stands in place of one rather than the question
being left open.

… missing

0030 decides that the password exists between the caller handing it over and
the request carrying it being written, that the route adds no error vocabulary
of its own, and that no branch of it returns a session with a field missing.
None of that was in the tree. `src/session/password.rs` holds the part a type
and a closure over three values settle.

The password is neither `Copy` nor `Clone`, has no accessor and no printable
form, and `present` takes it by move, so the three shapes 0030 names as the ones
that keep a credential alive - a credentials object held for a retry, a field on
the session for a renewal, a parameter threaded through a helper so a test can
sign in twice - are refused by the compiler rather than by a review. It performs
no scrub and claims none: 0030 takes a plain string and states that residual,
and this states it in the same words.

`what_the_answer_yields` is 0004's closure rule at this door. A body that parsed
and omitted the token, the account identifier or the validity statement is
`answer-not-understood` at the answer body, and the facts a session needs are a
type whose three members are all present, so an incomplete session has nothing
to be returned in. A value that arrived empty is read as one that did not
arrive, which is this module's reading rather than a sentence in 0030 and says
so where it is made.

`Failure::from_status_with_no_token_presented` is the third half, in the module
0037 fixes as the one mapping point rather than beside the route. It moves the
401 payload alone and leaves every other row of the table as `from_status`
reads it. The comment inside that function said no caller knew whether a token
had been presented and that the flag was the honest half until one existed; a
caller exists now, and the comment says what the default is for instead.

What this prevents is a wrong payload three calls from where it was produced: a
sign-in reporting a refused credential as a token presented and rejected is read
by 0034 as a session that has ended, which answers a person's mistyped password
with a renewal for a session that was never established.

#30 stays open. Its condition is a sign-in against the fake server in #21, and
the transport that would carry one is #27.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex added security architecture Shape of the core: boundaries, interfaces, contracts labels Aug 31, 2026
@iderex iderex self-assigned this Aug 31, 2026
@iderex iderex added this to the M3 Server access and session milestone Aug 31, 2026
@iderex
iderex merged commit dde9e62 into main Aug 31, 2026
24 checks passed
@iderex
iderex deleted the the-passwords-life-and-the-facts-a-session-needs-30 branch August 31, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

architecture Shape of the core: boundaries, interfaces, contracts security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant