Conversation
|
Thanks @antonmos — the investigation here is excellent. Proving the server I checked the load-bearing parts and they hold up:
Two things I'd like changed before merge, one follow-up, and one heads-up. 1. Make it a no-progress deadline, not a total budgetThis is the substantive one, and I think it makes your own offer to tighten the The wedge you diagnosed is a client sending zero bytes. An idle deadline The doc comment defends 30 s with "the slowest observed real handshake I might be wrong about this, and you have the logs: the PR body separately The failure mode if the number is wrong is the bad kind: finalize dies at the The per-step version is cheap — everything it needs is already publicI went looking for what this would cost, expecting to have to argue for it, and
So the loop can just live here, no acceptor change and no new acceptor let mut buf = WriteBuf::new();
let result = loop {
if let Some(result) = acceptor.get_result() { break result; }
tokio::time::timeout(FINALIZE_STEP_TIMEOUT, single_sequence_step(&mut framed, &mut acceptor, &mut buf))
.await
.map_err(|_| /* same warn + bail as today */)??;
};That's about ten lines replacing the fifteen already in the diff, and the Worth noting this is plausibly upstreamable, in the same family as #1515 and If you'd rather ship the bound now, I'll take your own offer instead: raise 2. Divergence number collision — (24) is takenHeads-up rather than a code problem: I have an unmerged branch that also claims 3. Follow-up: this bounds only the last leg, and the unbounded half is pre-authNot asking for it in this PR, but worth recording. On the serving path, So the same connect-then-go-silent wedge one step earlier still parks the 4. Minor: the test hangs instead of failingsrc/conn_test.rs:1222 — |
|
Filed the pre-auth follow-up from section 3 as #186 — |
TODO.md - In flight: review state of @antonmos's open PRs. #183's blocker is resolved in c8712e9 with one follow-up requested (the connection reset also fires on every reactivation; the latch survives a reset when no modifier is held; a pre-existing phantom drag after a mid-drag disconnect, latent on main too). #182 asked for a per-step deadline, with a ship-now fallback, and collides with the mic branch on divergence (24). #181 and #184 not yet reviewed. Links issue #186. - Upstreaming watch: a dated update superseding the stale "zero open PRs" note. Divergence (23) is upstream via #1476 + #1913 (ConnectionPolicy) with its traps; IronRDP#1969 blocks de-vendoring (22)/(23) and upstreaming (18) and ships in 0.14.0 unless fixed; #1483 closed; overlapping upstream work to evaluate at the next bump (#1951/#1953/#1954 multitransport stack, ironrdp-rdpeai #1645 + #1946 for the mic divergence). vendor/ironrdp-server/CLAUDE.md - (18): upstream status — #1484 green-lit by a peer, held patch 392 commits stale, and blocked on #1969 because upstream Preempt takes the handler for the race. Notes the --fork-workers scope boundary no longer applies. - (12): the #1951 -> #1953 -> #1954 stack as a possible partial de-vendor path, and how it differs from this divergence. Docs only.
…ering - Records the numbering decision: three branches claim vendored divergence (24), so by expected merge order #183 keeps (24), #182 takes (25) and the mic divergence becomes (26); otherwise take the next free number at merge. - #183: round 3 addressed all three follow-ups (verified against 334cc3c, CI green). Remaining asks: document and live-test the synthetic button-up, which lands at the pre-disconnect cursor position and can complete an interrupted drag as a drop; and tests for the connection-edge path. - #182: no reply yet; must renumber to (25). - #184: stacked on #183, review after it merges. - Upstreaming watch: mic divergence (24->26). Docs only.
|
Renumbered the finalize divergence (24) → (25) per your note (#183 keeps (24), your mic branch (26)) — pushed as On section 4 (the test hanging instead of failing): I tried the 120 s 🤖 Addressed by Claude Code |
|
Thanks — renumber confirmed, and the only On section 4, you're right and I'm withdrawing it. I reproduced it on your head: with the 120 s The duplex follow-up is the right shape, and cheaper than it sounds: Still open from the earlier comment: the per-step (no-progress) deadline versus the 30 s total budget — that's the substantive one. Unrelated heads-up on your red CI: the |
|
On per-step vs the 30 s total budget — I'd rather this live upstream than in the vendored fork. The bound already merged into IronRDP itself: Devolutions/IronRDP#1890 landed the total-budget On the "is 30 s too tight?" worry: the datapoint settles it. The 12.55 s-CredSSP cellular link you flagged is one of the 197, and its finalize was 0.53 s — finalize doesn't inherit CredSSP's round-trip cost, so the max finalize across all 197 was 4.97 s and 30 s keeps ~6× margin even on that link. Total-budget is safe. Per-step is still the nicer design, and I agree its home is upstream ("land it there rather than carry a total-budget divergence indefinitely") — as a follow-up on IronRDP it hardens every server, and macrdp still just drops the divergence on the bump. Given that, this doesn't need more automated iteration. Could you take a human review pass and call the disposition? I'm leaning hold-for-the-pin-bump, but I'm happy to merge the total-budget vendored form now if you'd rather #182 land on its own. |
|
I took a quick look: agreed on waiting for the pin bump, don't reshape, don't land the vendored form. Reasons for the decision:
Numbering: with this held, (25) frees up. #183 keeps (24) and my mic branch takes (25); I'll update my markers. Leaving this open rather than closing it — the regression test and the diagnosis write-up are worth keeping attached to a live PR, and it doubles as the reminder at bump time. Please don't delete the branch. One related thread: #186 (the unbounded accept_begin/TLS/CredSSP awaits on the serving path) is the half that isn't upstream. Same play would work there — upstream it and harvest it — if you want to pick it up. |
…to (24)/(25) Decided 2026-09-17 (comment 5722493925): hold #182 rather than reshaping it or landing the vendored form. The identical 30 s bound is already upstream in Devolutions/IronRDP#1890 (merged 2026-09-04 by @antonmos, same const, same inner accept_finalize call), so the pin bump harvests it and vendored divergence (25) never has to exist. Exposure until the bump is mild: macrdp preempts unconditionally and eviction isn't gated on the incumbent having activated, so a finalize-wedged client is displaced by the next authenticated connection. His log data also settled the 30 s margin question. The PR stays open on purpose — it holds the regression test and the wedge diagnosis, and acts as the bump reminder; the TODO notes what to retrieve. With #182 never landing a divergence, numbering is back to two claimants: #183 keeps (24) and the mic divergence takes (25). Docs only.
b031da8 to
5288581
Compare
…r this to (25) PR clintcan#182 (@antonmos, "bound accept_finalize") claims vendored-server divergence (24) for its FINALIZE_TIMEOUT. This branch already claims (24) for MS-RDPEAI microphone redirection. Neither number exists on main, so both branches saw it as free. clintcan#182 is review-ready while this branch still needs P3, so clintcan#182 lands first and keeps (24); the mic divergence becomes (25). Recorded at the divergence heading so the rename isn't missed when this branch rebases onto a main that carries clintcan#182 — two (24)s in the log is the same bookkeeping slip that produced clintcan#179 at pin-bump time. Docs only.
A third branch claims vendored divergence (24): PR clintcan#183's per-served-connection input-reset handle, alongside PR clintcan#182's FINALIZE_TIMEOUT and this branch's MS-RDPEAI processor. Decided by expected merge order: clintcan#183 keeps (24), clintcan#182 takes (25), and this divergence becomes (26). Updates the collision marker at the divergence heading and the P3 checklist item, and records the rule if the order changes: take the next free number on main at merge time. Docs only.
clintcan#182 was held for the pin bump on 2026-09-17 rather than landing its vendored divergence — the identical bound is already upstream in IronRDP#1890, so macrdp harvests it. That leaves clintcan#183 keeping (24) and frees (25) for this divergence. Updates the collision marker and the P3 checklist item. Docs only.
5288581 to
cdcace6
Compare
|
Thanks for keeping this rebased. One thing after the rebase: #182 now uses divergence (24), which #183 already claims (it's been (24) there since 09-16). With the mic taking (25) in #191, this one should be (26). Separately, just confirming the plan from 09-17 still holds: we wait for the pin bump to pick up the upstream bound (Devolutions/IronRDP#1890) rather than merge this form. If something has changed, like the hang recurring often enough that waiting isn't acceptable, let me know and we can reconsider. |
|
Heads-up: my docs commit |
cdcace6 to
dc16021
Compare
The roadmap still said "Not started" and stopped at the July 31 h soak. - Status header: Tier 1 done, Tier 2 done except the 48-72 h soak target. - New Tier 1.5 (adversarial hardening): fuzzing, --max-client-size, the four abuse harnesses, the two pre-auth DoS fixes (v0.9.4, #180), daily cargo-deny, the SIEM audit stream. - Tier 2.4: adds the 27.4 h v0.9.5 soak (0 restarts/panics, no leak) and states what's still unsoaked; both soak-tooling notes are done. - Tier 2.5: #180/#182 bounds, the #169 detach restart stopgap. - Tier 3: metrics surface partly done (--stats-endpoint + Status tab); upstreaming well along (22 merged, 5 forks left). - README: 250+ tests; mention the soaks and abuse testing.
|
Thanks for the (26) renumber and the rebase. Two small things:
|
… session slot Windows App for macOS build 68614 intermittently hangs the user at "Configuring remote session". ironrdp_acceptor=trace shows the server completing the ENTIRE handshake — X.224, TLS, CredSSP (incl. the HYBRID_EX EarlyUserAuthResult), MCS Connect, Erect Domain, Attach User and all seven channel joins — then blocking on a read, because the client never sends its Client Info PDU. That part is client-side and not fixable here: the server's ConnectResponse is byte-identical between hung and successful connections, tcpdump during a live hang captured nothing, and the socket sat ESTABLISHED with Recv-Q=0 AND Send-Q=0. Only restarting the client clears it — same class as the mstsc EGFX surface-retention quirk. The server bug is that it parked on that read forever. All three accept_finalize call sites (serve_negotiated, both arms of run_connection) were unbounded awaits, so a wedged client held the live-session slot — one such connection sat there 45 minutes. clintcan#180 bounded only the pre-auth candidate probe (CANDIDATE_NEGOTIATION_TIMEOUT) on the other half of the accept path. Bounded by FINALIZE_TIMEOUT (30 s). Placement is load-bearing: the timeout goes on the INNER ironrdp_acceptor::accept_finalize call, NOT on RdpServer::accept_finalize or its callers — that method is a loop whose body calls client_accepted, which runs the entire live session, so a timeout hoisted any higher would cap session length. Per-pass placement also gives a deactivation-reactivation its own budget and bounds one that wedges the same way. Refuted en route, recorded in the divergence log so they are not re-chased: path MTU (ping -D -s 1472 from the client succeeds), a missing HYBRID_EX EarlyUserAuthResult (the trace shows result=Ok(()) on the hung connections too), client-type correlation (the iOS client also hangs), network-path correlation (the same LAN IP both hangs and succeeds), and server startup state. New regression test a_client_that_wedges_during_finalize_is_dropped, verified to fail without the fix (it HANGS, killed at 150 s) and pass with it in 0.04 s. It uses start_paused so the 30 s bound costs no wall-clock time, which means it proves a bound exists, not the specific duration. tokio/test-util added as a dev-dependency only, so it never reaches the shipped binary. 206 passed, clippy -D warnings clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ence (26) (24) is reserved for clintcan#183 and (25) shipped as the mic redirection. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
decf9fe to
df6a22b
Compare
… the header Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks — both fixed in 1d0228b:
Rebased onto current main; 🤖 Generated with Claude Code |
What
Bounds
accept_finalizewith a newFINALIZE_TIMEOUT(30 s), so an authenticated client that wedges mid-handshake can no longer hold the live-session slot indefinitely. Vendored-server divergence (24).Why
Windows App for macOS build 68614 intermittently hangs the user at "Configuring remote session".
ironrdp_acceptor=traceon a live hang shows the server completing the entire handshake — X.224, TLS, CredSSP (including the HYBRID_EXEarlyUserAuthResult), MCS Connect, Erect Domain, Attach User and all seven channel joins — and then blocking on a read, because the client never sends its Client Info PDU.That part is client-side and not fixable here:
ConnectResponseis byte-identical between hung and successful connections;tcpdumpduring a live hang captured nothing, with the socket ESTABLISHED and Recv-Q=0 and Send-Q=0;Same class as the documented mstsc EGFX surface-retention quirk: stale client-process state.
The server bug is that it parked on that read forever. All three
accept_finalizecall sites (serve_negotiated, both arms ofrun_connection) were unbounded awaits, so a wedged client held the session slot — one such connection sat there 45 minutes. #180 bounded only the pre-auth candidate probe (CANDIDATE_NEGOTIATION_TIMEOUT) on the other half of the accept path; this is the same hazard on the serving path.Reviewer notes
Placement is load-bearing. The bound goes on the inner
ironrdp_acceptor::accept_finalizecall, not onRdpServer::accept_finalizeor its callers. That method is a loop whose body callsclient_accepted, which runs the entire live session — a timeout hoisted to the loop or a call site would cap session length. Per-pass placement also gives a deactivation–reactivation its own budget and bounds one that wedges the same way.Why 30 s. Measured across 197 successful connections in the field logs: median finalize 0.07 s, all but 13 samples under 1 s, slowest ever 4.97 s. So 30 s is ~6× the observed worst case. It is deliberately generous because the same budget covers reactivations (live resize, blank recovery), whose tail is not measurable from these logs — the fingerprint only fires on the first pass. A false timeout is cheap (drop + auto-reconnect); a false drop of a live session mid-reactivation is not. Open to tightening to ~15 s, or making it env-tunable (
MACRDP_FINALIZE_TIMEOUT_SECS) in the style ofMACRDP_GUARD_*/MACRDP_BLANK_RECOVERY_*— say the word.Note this bounds the damage, it does not fix the hang. The client still wedges; it now gets dropped in 30 s instead of parking the slot. The cure remains restarting the client.
Theories refuted en route (recorded in the divergence log so they are not re-chased): path MTU (
ping -D -s 1472from the client succeeds); a missing HYBRID_EXEarlyUserAuthResult(the trace showsresult=Ok(())on the hung connections too); client-type correlation (the iOS client also hangs — 4/4 from one address, 3/3 fine from another); network-path correlation (the same LAN IP both hangs and succeeds); server startup state (the first connection after a restart hung).Testing
New regression test
a_client_that_wedges_during_finalize_is_dropped— a client completes X.224 + TLS then deliberately never sends its Connect Initial.#[tokio::test(start_paused = true)]so the 30 s bound costs the suite no wall-clock time. Caveat, stated in the divergence log: paused time makes it prove a bound exists, not the specific duration.tokio'stest-utilis added as a dev-dependency only —cargo builddoes not pull dev-deps, so it never reaches the shipped binary.Full suite 206 passed, 0 failed;
cargo clippy --all-targets -- -D warningsclean;cargo fmtapplied.🤖 Generated with Claude Code