Skip to content

fix(web): prevent USB Serial teardown reset (#204) - #206

Merged
miso-develop merged 4 commits into
mainfrom
task/204-web-serial-control-lines
Sep 18, 2026
Merged

miso-develop merged 4 commits into
mainfrom
task/204-web-serial-control-lines

Conversation

@miso-develop

Copy link
Copy Markdown
Owner

Addresses #204

Root cause boundary

Physical evidence on the previous exact Web/Firmware baseline showed:

  • reload and tab close visibly restart the still-powered M5StickS3;
  • reconnect therefore reports LOCKED and requires fresh physical Unlock;
  • explicit Lock & Disconnect correctly reports LOCKED;
  • plain USB removal while battery keeps the Device runtime alive remains UNLOCKED;
  • automatic Lock was disabled.

Device-domain tracing found no transport-disconnect -> runtime-lock path. The causal surface is the ESP32-S3 USB Serial/JTAG CDC control-line state used by Web Serial teardown.

ESP32-S3 USB Serial/JTAG treats RTS=1,DTR=0 as reset. The mitigation must therefore never clear DTR while RTS may still be asserted.

Candidate Web fix

SerialPortLike now exposes optional Web Serial setSignals().

Each successful Web Serial connection establishes the safe host-control state using separate ordered operations:

  1. setSignals({ requestToSend: false })
  2. only after that succeeds, setSignals({ dataTerminalReady: false })

SerialSession.close() re-establishes the same order immediately before reader/writer release and port.close().

Safety behavior:

  • never uses a combined DTR+RTS call;
  • if RTS deassertion fails, DTR is not changed;
  • if DTR deassertion fails after RTS is known false, resource/port teardown still proceeds;
  • browsers without setSignals() retain the existing close path rather than failing transport cleanup.

Keeping the safe state from connection time also avoids relying solely on asynchronous pagehide work completing before the browser tears the page down.

Preserved boundaries

Unchanged:

  • explicit device.lock on Lock & Disconnect;
  • page lifecycle transient-secret cleanup;
  • transport/session teardown semantics;
  • automatic Lock;
  • reboot/power-loss RAM-only VMK destruction;
  • Protocol / Vault / credential behavior;
  • firmware / eFuse / USB Serial/JTAG Device configuration.

Regression coverage

serial.test.ts now verifies:

  • safe RTS -> DTR ordering is established immediately after open;
  • the same ordering is re-established before port.close();
  • DTR is not touched when RTS deassertion fails;
  • DTR failure after safe RTS state does not prevent deterministic port close.

Existing #196 lifecycle tests continue to prove:

  • page lifecycle uses transport-only disconnect and clears transient state;
  • lifecycle teardown does not send device.lock;
  • explicit close still sends one device.lock before transport close.

Base / collision

Required Human Gate

This PR remains Draft until the exact candidate head is physically verified with the same Chrome/M5StickS3 scenario.

Required exact-head checks:

  • reload: no visible Device restart; reconnect hello.state remains UNLOCKED; no fresh physical Unlock
  • tab close: same
  • navigation away: same where practical
  • explicit Lock & Disconnect: still LOCKED
  • USB removal while battery keeps runtime alive: still UNLOCKED
  • reconnect after lifecycle teardown has no stale Web Serial ownership/deadlock

If port.close() still causes a reset despite the safe control-line state, the next #204-approved experiment is page-lifecycle-specific omission of explicit port.close(), not a Device security relaxation.

CI evidence pending.

Copy link
Copy Markdown
Owner Author

Implementation candidate evidence — Issue #204

Exact candidate HEAD: 0c0f476fd40cee0e537e36636775d3680cf4eedd

Web Serial mitigation

  • SerialPortLike exposes optional setSignals().
  • Every successful connection establishes the safe ESP32-S3 USB Serial/JTAG host-control state using separate ordered calls:
    1. requestToSend: false
    2. only after RTS succeeds, dataTerminalReady: false
  • SerialSession.close() re-establishes the same safe ordering immediately before stream release and port.close().
  • No combined RTS/DTR call is used.
  • If RTS deassertion fails, DTR is not touched.
  • If DTR deassertion fails after RTS is known false, deterministic resource/port close still proceeds.
  • The safe state is established at connection time so correctness does not rely only on async pagehide work completing.

Preserved security/lifecycle boundaries

  • Explicit Lock & Disconnect still sends device.lock.
  • Page lifecycle still uses transport-only teardown and sends no implicit device.lock.
  • Browser transient-secret cleanup remains unchanged.
  • Automatic Lock, reboot/power-loss VMK destruction, Protocol/Vault behavior, firmware, eFuse, and Device USB configuration are unchanged.

Regression evidence

  • Web unit suite: 38 files / 304 tests passed.
  • serial.test.ts: 15 tests passed, including four new control-line ordering/failure cases.
  • Existing browser-lifecycle.test.ts: 4 tests passed.
  • Linux Chrome production smoke: SUCCESS.
  • Windows Chrome production smoke: SUCCESS.
  • Normal Web build / Web lockfile verification: SUCCESS.
  • Firmware native/security regressions: SUCCESS.
  • Isolated firmware build/merge, distribution-package validation, and ESP-IDF lock verification: SUCCESS.

Exact-head CI

  • Security #934 / run 35354234460: SUCCESS.
  • Foundation #931 / run 35354234467: SUCCESS.
    • web: SUCCESS
    • web QR Windows Chrome: SUCCESS
    • firmware: SUCCESS

Collision / staleness

Remaining gate

Physical verification is still mandatory because CI cannot reproduce the Windows Chrome -> ESP32-S3 USB Serial/JTAG control-line transition. PR remains Draft and is not READY_FOR_REVIEW until the exact candidate Web head passes the #204 Human Gate.

Copy link
Copy Markdown
Owner Author

Human Gate ownership update

The human owner has directed that the #204 physical Human Gate be executed under the Integration role rather than directly from the Implementation chat.

Current exact candidate remains:

The previously recorded physical checks and exact-head setup remain authoritative. Integration should perform/coordinate those checks on this exact head and persist the Human Gate result durably.

No additional implementation change has been made after the candidate CI completed. PR #206 remains Draft while the Human Gate is unresolved.

Copy link
Copy Markdown
Owner Author

Integration Human Gate reconstruction

Exact candidate HEAD: 0c0f476fd40cee0e537e36636775d3680cf4eedd
Current main / PR base: 40f7061eb89dcc676599d7f74f1b45037e589ea2

Remaining gate: exact-head physical validation of Windows Chrome -> ESP32-S3 USB Serial/JTAG control-line teardown behavior.

HANDOFF
FROM: integration
TO: human
ISSUE: #204
PR: #206
HEAD: 0c0f476
STATE: HUMAN_GATE_REQUIRED

Copy link
Copy Markdown
Owner Author

Human Gate completed

Exact validated HEAD: 0c0f476fd40cee0e537e36636775d3680cf4eedd

  • Reload: PASS — no Device restart, reconnect succeeds, state remains UNLOCKED, no fresh Unlock.
  • Tab close: PASS — no Device restart, reconnect succeeds, state remains UNLOCKED, no fresh Unlock.
  • Navigation away: PASS — no restart, reconnect succeeds, state remains UNLOCKED.
  • Explicit Lock & Disconnect control: PASS.
  • USB removal while battery keeps runtime alive: PASS.
  • Localhost active-writer conflict / disabled Unlock control is expected for the distinct localhost origin and was not bypassed via recovery/reconciliation.
  • Foundation #931: SUCCESS.
  • Security #934: SUCCESS.

Human Gate no longer blocks Review.

@miso-develop miso-develop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review disposition: READY_FOR_INTEGRATION

Reviewed exact PR head 0c0f476fd40cee0e537e36636775d3680cf4eedd against Issue #204, Decision #195, Requirement #137/51, Decision #49, the durable Device→Web root-cause handoff, and the completed exact-head Human Gate.

No blocking finding identified.

Root-cause / platform boundary:

  • The original physical reproduction used exact then-current Web/Firmware build b3aa8fc0cc06143990dcf32a971853264f73eba8, Chrome 153.0.8010.48 Stable 64-bit, and Automatic LOCK disabled.
  • Reload and tab close visibly restarted the Device; reconnect then correctly reported LOCKED and required fresh Unlock.
  • Explicit Lock & Disconnect remained LOCKED, while physical USB removal with battery preserving the same runtime remained UNLOCKED.
  • This distinguishes the defect from stale UI, stale build, automatic-lock expiry, or ordinary transport-disconnect locking: the unwanted LOCKED observation was caused by an actual browser/page-teardown-correlated Device reset.
  • Device-side tracing remains non-locking for transport teardown. The exact firmware used in the candidate Human Gate (46802e2baa9e03cd1331244885b9e9c76f005a85) and current PR base have the same relevant boundary: USB disconnect calls protocol/session teardown, while CanonicalProtocolV2Handler::disconnect() cancels session/pending state without calling runtime_.lock().
  • Independent external check agrees with the durable diagnosis:

Implementation verification:

  • SerialPortLike adds only optional Web Serial setSignals(); browsers lacking it retain the pre-existing transport path.
  • Every successful serial connection invokes control-line normalization before the initial Protocol-v2 hello.
  • Normalization performs two distinct awaited operations:
    1. setSignals({ requestToSend: false });
    2. only after that succeeds, setSignals({ dataTerminalReady: false }).
  • No combined RTS/DTR update is used.
  • The same safe sequence is re-established immediately before stream/port teardown.
  • If RTS deassertion fails, the function returns without touching DTR.
  • If DTR deassertion fails after RTS is known deasserted, teardown continues; this does not create the known RTS=1/DTR=0 transition.
  • Existing reader/writer cancellation/release and deterministic port.close() behavior are otherwise unchanged.
  • Connection-time normalization means correctness does not depend solely on asynchronous pagehide cleanup finishing before browser teardown.

Security/lifecycle boundaries preserved:

  • cleanupBrowserLifecycle() still performs transient-state cleanup then transport-only teardown.
  • CanonicalDeviceManagement.disconnectTransport() sends no device.lock.
  • Explicit CanonicalDeviceManagement.close() still sends exactly one device.lock before transport close.
  • Automatic Lock behavior is unchanged.
  • Actual reboot/power loss still destroys the RAM-only VMK and starts LOCKED.
  • Protocol, Vault, TOTP, Trusted Browser authorization, credential handling, firmware, eFuse, and Device USB configuration are unchanged.
  • No relaxation of a Device security boundary was introduced; this fix prevents an unintended hardware reset so the already-approved same-runtime lifecycle semantics from #195 can actually hold.

Regression coverage:

  • New serial tests prove safe RTS→DTR ordering immediately after open and again before close.
  • Regression explicitly proves DTR is not changed when RTS deassertion fails.
  • Regression proves DTR failure after safe RTS does not prevent deterministic close.
  • Existing lifecycle tests independently prove:
    • browser lifecycle cleanup uses transport-only disconnect;
    • no device.lock is emitted on page lifecycle teardown;
    • explicit close still emits one device.lock.

Exact-head Human Gate:

  • Exact Web candidate: 0c0f476fd40cee0e537e36636775d3680cf4eedd.
  • Chrome: 153.0.8010.48 Stable 64-bit.
  • Reload: PASS — no Device restart; reconnect succeeds; state remains UNLOCKED; no fresh physical Unlock.
  • Tab close: PASS — same.
  • Navigation away: PASS — no restart; reconnect succeeds; state remains UNLOCKED.
  • Explicit Lock & Disconnect positive control: PASS.
  • USB removal while battery preserves runtime: PASS.
  • No stale Web Serial ownership/deadlock was observed.
  • The localhost active-writer conflict is expected origin isolation; no Recovery/reconciliation or Trusted Browser replacement was used to bypass it.

Exact-head CI:

  • Foundation #931: SUCCESS.
  • Web unit suite: 38 files / 304 tests passed.
  • serial.test.ts: 15 tests passed.
  • browser-lifecycle.test.ts: 4 tests passed.
  • Linux Chrome production smoke, normal Web build/lockfile verification, Windows Chrome smoke, firmware regressions, isolated ESP-IDF build/package validation, and dependency-lock verification succeeded.
  • Security #934: SUCCESS, including existing lifecycle/security/release contracts and repository scan.

Staleness/collision:

  • Handoff HEAD matches current PR HEAD.
  • PR is open, mergeable, and exact base/current main is 40f7061eb89dcc676599d7f74f1b45037e589ea2.
  • Compare is 4 commits ahead / 0 behind.
  • Changed files are exactly web/src/serial.ts and web/src/serial.test.ts.
  • Open PR #209 is README-only and PR #210 is Web presentation/style-only; neither overlaps Web Serial transport ownership.

Repository state:

  • PR remains Draft. This is not a Review blocker under the current Integration contract: Integration's typical input is a PR marked ready after Review.
  • Integration must transition PR #206 out of Draft before merge and re-check HEAD/checks/collision at that point.

No additional Security-role review is required: the governing lifecycle/security semantics were already resolved, the patch does not weaken Lock/VMK boundaries, Security CI is green, and the mandatory physical gate has passed on this exact Web head.

Copy link
Copy Markdown
Owner Author

HANDOFF
FROM: review
TO: integration
ISSUE: #204
PR: #206
HEAD: 0c0f476
STATE: READY_FOR_INTEGRATION

@miso-develop
miso-develop marked this pull request as ready for review September 18, 2026 15:07

Copy link
Copy Markdown
Owner Author

READY_TO_MERGE

HEAD: 0c0f476fd40cee0e537e36636775d3680cf4eedd
Current main: 3432bcda8af5a4c4a09bafadebec9ae1484f9460

Integration reconstruction:

STATE: READY_TO_MERGE

@miso-develop
miso-develop merged commit 386baf9 into main Sep 18, 2026
4 checks passed
@miso-develop
miso-develop deleted the task/204-web-serial-control-lines branch September 18, 2026 15:07

Copy link
Copy Markdown
Owner Author

INTEGRATED

PR: #206
Pre-merge HEAD: 0c0f476fd40cee0e537e36636775d3680cf4eedd
Squash merge commit / current main: 386baf91d41e8c6c87e896b328bc2ddbea3753c2

Integration evidence:

STATE: INTEGRATED

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