Skip to content

A session whose owner hung up is not handed again, and an open asks again for one that ended - #1206

Merged
Japabu merged 3 commits into
mainfrom
desktop-reopen-ended-session
Nov 11, 2026
Merged

Japabu merged 3 commits into
mainfrom
desktop-reopen-ended-session

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

session_lifecycle stalled once at "the desktop to open again"
(issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md).

The compositor asks for a desktop session the moment its control connection
ends. The supervisor answered the screen's row with its open desktop session
unless that session's owner had exited, by a non-blocking wait. The kernel
closes an ending process's handles before it publishes its exit
(teardown_resources before publish_climb), and publishes only after every
end below it. An ask in between was handed the ended session: its control
port answered Gone, the compositor said no desktop session: … Gone; the next launch asks again, and opened none until something launched.

Resulting behavior

  • A session whose owner hung up is not handed again. The supervisor
    keeps the hang-up of the owner's link, which the same close of handles
    makes (Link::HungUp). The loop also stops watching a link that would be
    readable for good.
  • An open asks again for a session that ended, at once and for at most
    5 s. toyos_session::open asks the supervisor again when the session it
    was handed has no controls left. The supervisor's reading can be outrun by
    an owner ending after it answers, more than once; Gone says so.
  • The asking ends with a session whose controls connect, with the
    supervisor's refusal, or after ENDED_UNREAD (5 s) as an error that names
    the state and the number of asks.
  • This applies to every login authority, not only the screen's row. For
    a remote row Gone means a fresh owner failed, so one failed login now
    retries in a burst until the budget (3 failures in 10 s) or the room
    refuses it.

Decisions

  • The kernel's order stays. An exit is published after every end below it by
    design, so handles close first.
  • The control connection stays the compositor's to connect, so it is still
    counted to the compositor's job.
  • The decision takes both signs of an owner's end (sessions::Open's
    hung_up and exited).
  • toyos-session now names toyos-abi for SyscallError::Gone.

Checks

session_lifecycle on the guest runner (QEMU x86-64). Round 2, at the head
9b3e6e286; every control is a patch on that head.

check revision result
the head 9b3e6e286 run 42941811828, green
control: the owner closes its controls and never exits dfb8c1aa9 run 42941817419, red: the supervisor handed over a session whose controls are gone 145833 times in 5 s
control: the kernel spin, no re-ask e47bb21c3 run 42944210533, green
control: the kernel spin, no re-ask, the decision ignoring the hang-up d85c16217 run 42945003574, green
cargo run -- --ci host, macOS 9b3e6e286 109 of 110 green; the red is issues/the-llvm-environment-test-breaks-git-on-macos.md

Both kernel-spin controls are green, so the question the review asked
stays open.
With the re-ask removed, the spin leaves the row green whether
or not the decision reads the hang-up. The spin therefore does not reproduce
the stall, and its first arm shows nothing about Link::HungUp. Why the
spin does not reproduce is not established. Link::HungUp is still held in
a guest by nothing: the host test with its control holds the decision, and
the wiring is the two sites the review read.

Round 1, at c6f8f6667, still stands for what it showed:

check revision result
control, base: main plus an owner that closes its controls 300 ms before it exits aaa0bc08b run 42928615028, red: no desktop session: … Gone, then the stall
control, fix: the branch plus the same owner 68cabe5b4 run 42928621680, green
the decision's host tests 9a234ec06 7 passed, exit 0
the same with the decision ignoring the hang-up 9a234ec06 exit 101: Some(0) for None

Evidence: r2 https://github.com/ToyOSOrg/evidence/tree/09d8692598f285c4e0cfa894e55f9a7be723929e/r2, r1 https://github.com/ToyOSOrg/evidence/tree/09d8692598f285c4e0cfa894e55f9a7be723929e/r1

Review r1

  • Blocker 1: the re-ask has a bound and says its expiry; the doc comment
    says what ends the asking. The expiry control above shows the message.
  • Blocker 2: tests/guest/rust-tests/Cargo.lock lists toyos-abi under
    toyos-session.

Records

Closes
issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md.
Nothing outside issues/ cited it.

Risks

  • While the supervisor has read neither sign, open asks in a loop of
    supervisor round trips with no delay between them: about 29 000 a second
    in the expiry control, for 5 s at most.
  • No manual page changes: docs/ does not describe how a desktop session is
    reopened.

🤖 Generated with Claude Code

Japabu and others added 2 commits November 11, 2026 01:59
…gain for one that ended

The compositor asks for a desktop session the moment its control
connection ends. The supervisor answered the screen's row with its open
desktop session unless that session's owner had exited, by a
non-blocking wait. The kernel closes an ending process's handles before
it publishes its exit, and publishes the exit only after every end below
it, so an ask in between was handed the ended session: its control port
answered `Gone`, the compositor said `no desktop session: ... Gone; the
next launch asks again`, and opened none until something launched.

Two changes, at the two ends of the answer:

- The supervisor reads the owner's hang-up of its link, which that same
  close of handles makes, and keeps it (`Link::HungUp`): a session whose
  owner hung up is not handed again, and the loop stops watching a link
  that is readable for good. The decision takes both signs of an owner's
  end (`sessions::Open`), and its host test holds a hung-up owner whose
  exit is not published.
- `toyos_session::open` asks again when the session it was handed has no
  controls left. However current the supervisor's reading, an owner can
  end after it answers; `Gone` says so, and the next ask finds the end
  read or a new session.

Seen in guest-rows run 42089538985, where `session_lifecycle` stalled at
"the desktop to open again".

Closes issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A link is now either not yet connected or hung up, and the line said only the first.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1206 at 9a234ec06

SEND BACK on two blockers: open's re-ask has no bound of its own, and a tracked lockfile is left stale. The cause is established and the two-mechanism shape is right. It merges cleanly onto fb88cec2a; #1201 and #1208 touch none of these paths.

Architecture

  • Cause: established, by the incident's own message plus inspection, not by a control.
    • Gone comes only from a closed port (kernel/src/syscall/ipc.rs:227,249).
    • The controls' one acceptor is the owner's, held for its whole life (system/session/src/main.rs:47; it leaves only by process::exit), so it closes only in the kernel's teardown.
    • That teardown closes handles (kernel/src/process.rs:796) before it publishes the exit (:813), and the supervisor handed the session on a fresh non-blocking wait that said "not exited".
  • Both mechanisms are needed; neither is dead.
    • The re-ask is what fixes the stall. The supervisor's reading can be outrun: a round serves requests (system/supervisor/src/main.rs:1765) before it pumps links (:1837), and the controls' port closes separately from the link.
    • Link::HungUp alone therefore cannot close the window. What it does is end the re-ask at the hang-up rather than at publication, which waits for every end below the owner.
    • It also stops the supervisor's loop re-arming a link that stays readable, which by my reading spins on main between hang-up and exit (inference, not run).
    • The re-ask masks Link::HungUp completely: with that arm broken, every row and control here would still be green.
  • Owner and representation are right. The supervisor stays the authority on which session is the desktop's. The Login must outlive the hang-up for its exit code and the stop's count, so a third link state is the minimal form.
  • Size: production +23 lines net, tests +4, one issue record removed.
  • Manual: the sessions page says nothing on reopening a desktop session; no change is needed.

BLOCKER

  1. lib/session/src/lib.rs:284-298 — the re-ask loop has no bound and says nothing — a wait with no failure timeout, on an interface that can carry the error.

    • It ends only if the supervisor reads a sign. With the controls gone and neither sign read, it spins supervisor round trips for good, silently, holding the compositor's launcher thread so every queued launch waits.
    • The PR's own control is that state: it ran the loop for 300 ms and would have run it forever had the patched owner not exited.
    • The shipped owner cannot reach it, but that is an invariant in another program; if it breaks, this should fail loudly, not livelock.
    • The doc at :277-279 is wrong on two counts. "Which it reads next" is false: the supervisor can answer again first. "So the asking ends" covers only failing owners whose ends were already read.
    • Needed: a failure bound in open that returns an error naming the state, and a doc that says what is true. No delay is needed.
    • Evidence after the change: one session_lifecycle dispatch on the new head. For the expiry path, the existing owner control with its exit removed should go red showing the new message (one dispatch), unless inspection is argued sufficient.
  2. tests/guest/rust-tests/Cargo.lock:1047-1051 — toyos-session still lists only toyos — the manifest now names toyos-abi, and this build is not --locked, so every guest build rewrites a tracked file. Add the one line.

SUGGESTION

  • Hold Link::HungUp in a guest, and settle the kernel-spin control, with two dispatches. Take the branch plus control-kernel-spin.patch with the lib/session change reverted: green needs the hang-up arm. Add control-decision-ignores-hang-up.patch: red is the recorded stall, reproduced through the kernel's own order. If both are green, the spin still starves the askers and the question stays open.

The stated gaps

Gap Close before landing?
Guest control exercises the re-ask, not Link::HungUp No. The predicate is host-tested with a control, and the wiring is two lines I read (system/supervisor/src/sessions.rs:431-434, :360-365). The suggestion above closes it if wanted.
Kernel-spin control inconclusive No. The cause stands without it. The explanation stays a guess; the scheduler pulls on idle (kernel/src/sched/driver.rs:470), so "the askers could not run" is not self-evident.
Head not guest-tested No. c6f8f6667..9a234ec06 is one log string with no reader, and host CI compiled the head. The fix for blocker 1 does need the row again.

Other consumers of the same window

None stalls; nothing to record.

  • Compositor start (system/compositor/src/signout.rs:77-93): a Gone there falls through to an ask, which now re-asks.
  • A launch into a dying session: waits on the launcher port the supervisor holds and reads its end at publication.
  • A detached spawn under a dying owner: refused Gone by the kernel.
  • logout: lists a hung-up session as active until its exit is read, and a sign-out of it is refused "holds no link".

For the description

  • The re-ask applies to every login authority. For a remote row, Gone means a fresh owner failed, so one failed login now retries in a burst until the budget (3 failures in 10 s) or the room refuses it. Say so.
  • "So the asking ends" needs the same correction as the doc comment.
  • Verified as stated: the three guest logs and their revisions, the two host test logs, the macOS red being the recorded llvm test, and that nothing outside issues/ cited the closed record.

SEND BACK

…e guest lockfile names the dependency

`toyos_session::open` asked again for as long as the supervisor handed
over a session whose controls were gone: with neither sign of the
owner's end read it would have asked for good, silently, holding its
caller's thread. It now asks for at most `ENDED_UNREAD`, 5 s, with no
delay between asks, and then returns an error naming the state and the
number of asks. Its doc comment says what ends the asking.

`tests/guest/rust-tests/Cargo.lock` lists `toyos-abi` under
`toyos-session`, as the manifest has since the fix.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1206 at 9b3e6e286 (9a234ec06..9b3e6e286)

LAND. Both r1 blockers are resolved and the change adds no new one. Three optional suggestions follow. The round is +20/−4 lines in lib/session/src/lib.rs and one lockfile line; it merges cleanly onto 7b912fc72.

Blocker 1, the re-ask's bound: resolved

open now ends its asking after ENDED_UNREAD (5 s) with an error naming the state and the count (lib/session/src/lib.rs:273-315). The doc comment now says only what is true.

The bound is justified as a failure bound.

  • Its comment says what expiry means: the supervisor reads a hang-up in one round of its loop, so a session it still hands out after 5 s is one it does not read as ended.
  • The number is policy, as HANDSHAKE_TIMEOUT's 2 s is (system/supervisor/src/main.rs:177). Legitimate asking lasts a few round trips of about 34 µs each (145,833 in 5 s), so nothing depends on its exact value.

The re-ask without a delay is acceptable. I do not ask for an event.

  • In a working system it is not a spin. Each ask blocks on the supervisor's answer, which takes two rounds of its loop (main.rs:1725), and every round that finds the link readable pumps it (main.rs:1838). Once the kernel has closed the owner's link, at most one more ask is answered stale.
  • Before that, the asks span only the kernel's drop of one handle table, which waits on nothing (kernel/src/object/ops.rs:95-98).
  • The 145,833 asks are the one state in which no event is coming: the owner dropped its controls, lives and keeps its link. Nothing can be awaited there, so the bound is the only exit.
  • The cost in that state is about one core, shared by the launcher thread and the supervisor, for 5 s per open, with launches queued behind it. Each later launch asks again and pays it again (system/compositor/src/signout.rs:108-111). The supervisor kept serving and ran out of nothing over those asks.
  • Root's rule is met as written: no fixed delay, a wait with a failure timeout, and the expiry returned as an error.

Event-based forms exist, and each costs more than it saves. Smallest first:

  1. The supervisor keeps a copy of the controls' acceptor, as it does the launcher's. A connect then never answers Gone; the connection reads its end when the supervisor closes the session, and the compositor already asks again on that. This deletes the loop, the clock and the toyos-abi dependency. Against it: the compositor says "open again" for a dead session, the reopen waits for the exit's publication, and an owner that drops its acceptor fails silently instead of with Gone.
  2. open waits for the end of the launcher it was handed, under the same bound. It is client-only and about ten lines, but it uses a launch connection as a watch and also waits for publication.
  3. The supervisor holds its answer until it has read the end. It cannot know to hold it unless the ask says the last session was gone, so this needs a protocol word, a parked ask and a deadline in the supervisor's loop.

The first is the simplest. I would not take it: Gone is the exact fact, known at once, and that form hides it.

The error reaches a caller that reports it, in all three:

  • the compositor's log (signout.rs:135), which the expiry control shows;
  • sshserver, to the client's stderr (system/sshserver/src/connection.rs:315-318);
  • test-runner, by panic (tests/guest/test-runner/src/main.rs:43-45).

Blocker 2, the guest lockfile: resolved

tests/guest/rust-tests/Cargo.lock:1051 lists toyos-abi under toyos-session. The root Cargo.lock is the only other tracked lockfile naming the crate, and it already did.

The kernel-spin controls

I confirm r1: this gap need not close before landing. The PR body states it accurately.

  • The fix does not depend on which window produced Gone. open asks again on any Gone until the supervisor's reading catches up. The owner-side control of r1 reproduces the incident's message on main and is green on the branch.
  • The logs show why the spin opens no window: it holds back whoever would look through it. In both arms "a force ends the holder" comes 300 ms after the step before it (1 ms at the head), and ten opens and sign-outs take 2.7 s and 3.0 s (6 ms at the head). So the test-runner's wait for its control's end, or its next open, waits out each spin.
  • A likely mechanism, not established: a CPU spinning with preemption off runs no scheduler pass, and surplus is pushed to a halted sibling only from a pass (kernel/src/sched/driver.rs:470). A thread the closing CPU wakes then runs after that CPU's teardown, which is after the publication. That would also fit the incident's rarity: it needs a sibling to pull the asker first.
  • Link::HungUp stays held in a guest by nothing, as the body says. The host test with its control holds the decision, and the two wiring sites are unchanged since r1.

Evidence, checked

claim what I found
head green run 42941811828, head 9b3e6e286, PASS session_lifecycle
expiry control red run 42941817419 at dfb8c1aa9; the compositor's line arrives 5.000 s after the session's end, then the row stalls on the owner that never exits
both spin arms green runs 42944210533 at e47bb21c3 and 42945003574 at d85c16217
the control patches each published patch equals git diff 9b3e6e286 <control>
host log 1 of 110 red, the build system: cargo test --lib: llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key, the recorded macOS failure. The other FAILED lines are kernel-loom's negative controls. toyos-session's tests and the decision's test pass in it.

The host log names no revision. It starts 52 s after the head's commit time, which fits.

SUGGESTION

  1. lib/session/src/lib.rs:14 — "No clock ends anything." now sits in a module with a clock. It means a sign-out; say "No clock ends a sign-out".
  2. lib/session/src/lib.rs:291,305 — the clock starts before the first ask, but ENDED_UNREAD is documented as how long open goes on asking for a session whose controls are gone. If the supervisor is slow to answer the first ask, one Gone ends the open with "1 times in 5 s" and no second ask. Starting the clock at the first Gone makes the code match its comment.
  3. To hold Link::HungUp in a guest, use an owner-side control, since the kernel spin cannot. The owner drops its controls and its link 300 ms before it exits, with the re-ask removed: green needs the hang-up read. Add the decision ignoring the hang-up: red. Two dispatches, not needed to land.

LAND

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 02:58
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 1cab347 Nov 11, 2026
11 checks passed
@Japabu
Japabu deleted the desktop-reopen-ended-session branch November 11, 2026 03:14
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.

1 participant