fix(keepkey): validate Features version fields in initialize() before semver#37
Merged
Merged
Conversation
… semver
Without this guard, a Features payload missing major/minor/patch
(wrong-message-type miscast, decode mismatch, or device in an
unexpected state) caused initialize() to construct
`vundefined.undefined.undefined` and pass it to semver.gte(), which
threw the opaque error:
Invalid Version: vundefined.undefined.undefined
This error has been observed leaking from the WebUSB pair path in
KeepKey Vault on Windows with no actionable diagnostic in the engine
log. Replace it with a clear runtime error that names the missing
fields so callers can recognize the failure mode and surface a
useful UI state instead of a silent splash hang.
Includes a regression test (keepkey-initialize.test.ts) that
reproduces the exact original error string against the unfixed code
and verifies the new validation throws before semver is reached.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Capture rejection once instead of double initialize() call - Happy-path test now asserts resolve + verifies returned features Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BitHighlander
pushed a commit
to keepkey/keepkey-vault
that referenced
this pull request
Apr 8, 2026
Captures the diagnostic work from a 1.2.14 Windows install session for the next agent to act on. Three distinct failure modes, all surfacing to the user as the same "splash never advances" symptom but caused by unrelated bugs in different layers. 1. `Invalid Version: vundefined.undefined.undefined` on WebUSB pair — defensive fix in flight at keepkey/hdwallet#37 (with regression tests). Bumping the submodule pointer here is a follow-up after that PR merges. 2. Native crash on USB unplug during in-flight WebUSB write — bun process disappears with no JS error after libusb's `bad write`. Same family as the existing macOS attach-time SIGTRAP guard in engine-controller.ts:251-254. Open; suggested approach in the doc ranges from a process-supervisor band-aid to moving USB I/O into a child process. 3. Splash hangs forever when port 1646 is already bound — the FATAL exit message is in the backend log but the BrowserWindow is created before the port collision check, leaving an undecorated splash on screen. Open; fix is to move the collision probe before window creation and/or surface a "Vault is already running" UI state. Doc includes log evidence for each finding, root-cause file pointers, reproduction recipes, and an explicit "what is NOT broken" section to prevent the next agent from re-investigating the verified-intact download, the 1.2.14 @noble/hashes fix, the protobuf schema, and the frontend bundle.
This was referenced Apr 8, 2026
BitHighlander
pushed a commit
to keepkey/keepkey-vault
that referenced
this pull request
Apr 9, 2026
…ation
THE BUG
=======
projects/keepkey-vault/src/bun/index.ts:4246 unconditionally spawned
`bash -c '<heartbeat watchdog script>'` at module load. On Win10, when
the app is launched from Explorer / Start Menu / installer Run-now,
the bun worker inherits an empty PATH from the parent process.
Bun.spawn delegates to libuv, which fails with UV_ENOENT (-4058) when
it can't locate `bash`, and the failure is delivered as an UNCAUGHT
asynchronous exception in the worker thread several seconds after the
spawn call site. The exception kills the entire app right around the
device pair flow, and the user sees a splash that hangs and then
disappears.
The watchdog is POSIX-only — its script uses bash, kill -9, date +%s,
sleep, cat — and could never have functioned on Windows even if `bash`
were on PATH. It exists to defend against an FFI freeze in
kkemu confirm_helper that only happens on macOS/Linux builds.
This was the dominant Win10 1.2.14 first-launch crash. Reproduced
twice across two different rebuilt 1.2.14 binaries (4f8ec1ba… and
2111ad61…), and verified fixed in-place by patching the installed
bundle and re-launching from the desktop icon.
THE FIX
=======
1. startHeartbeatWatchdog() returns early on process.platform === 'win32'
with a [Vault] Heartbeat watchdog skipped on Windows log line.
2. The Bun.spawn(['bash', ...]) call is wrapped in try/catch as
defense-in-depth on POSIX hosts where bash could conceivably be
missing (containers, minimal NixOS, etc).
3. A long comment block above the function explains the platform
constraint and references this incident, so the next person to
touch the watchdog doesn't accidentally re-enable it on Windows.
OBSERVABILITY (the only reason this was diagnosable)
====================================================
The previous logger used fs.createWriteStream(LOG_FILE, {flags:'a'})
whose buffered .write() calls never reached disk on a worker-thread
crash. Every failed launch left a vault-backend.log that "ended" 2-7
seconds before the actual death point, and we spent two days chasing
wrong root causes (libusb segfault, semver throw, port collision)
because the actual exception line never made it to disk.
This commit replaces the buffered stream with fs.appendFileSync per
log call. Throughput hit is negligible at our log volume (~10-100
lines/sec at peak boot); the upside is that the log file is now a
faithful record of what executed up to a crash.
It also adds a structured boot environment dump immediately after
[Boot] Log file: …, capturing platform, arch, pid, ppid, cwd, argv,
stdio TTY status, PATH.length, LANG, LC_ALL, and Windows-only env
vars (USERNAME, SESSIONNAME, APPDATA, LOCALAPPDATA). The PATH.length
field is what surfaced this bug — Explorer launches show
PATH.length=0, terminal launches show PATH.length=882. Without that
single field, the only evidence was "splash hangs" which is
consistent with twenty different root causes.
Finally, engine-controller.ts gets boundary log lines around every
JS↔native transition in start() and fetchFirmwareManifest() —
usb.on(attach), usb.on(detach), usb.getDeviceList(), mergeManifests,
applyChannel, syncState — each wrapped in try/catch with an explicit
[Engine] FATAL: log so a future libusb segfault leaves a clear
breadcrumb instead of a silent process exit.
WHAT THIS DOES NOT FIX
======================
The three findings from docs/HANDOFF-1.2.14-WINDOWS-PAIR.md remain:
- Finding 1 (Invalid Version: vundefined.undefined.undefined) is
addressed by keepkey/hdwallet#37 which still needs review/merge,
followed by a submodule pointer bump here.
- Finding 2 (native crash on USB unplug during pairRawDevice) is
separate from this PR's bug. With the sync logger landing, the
next reproduction should leave the actual death cause in the log.
- Finding 3 (port-1646 collision splash hang) is already fixed in
the official 1.2.14 rebuild from 2026-04-09 — verified working.
See docs/HANDOFF-1.2.14-WIN10-WATCHDOG-CRASH.md for the full
diagnosis story, including the verification recipe.
6 tasks
BitHighlander
added a commit
to keepkey/keepkey-vault
that referenced
this pull request
Apr 9, 2026
Bump version to 1.2.15 and pin hdwallet submodule to master (includes keepkey/hdwallet#37 — Features version validation). Changes since v1.2.14 pre-release: - fix(win10): skip POSIX heartbeat watchdog on Windows (#107) - fix: sync file logger — crash logs now survive native exceptions (#107) - fix: boot environment dump for launch-context diagnostics (#107) - fix: JS↔native boundary logging in engine-controller (#107) - fix: port 1646 collision check before window creation (#106) - fix: USB detach race guard on WebUSB pair (#106) - fix: hdwallet Features version validation (hdwallet#37) - docs: handoff for 1.2.14 Windows pair failures (#105) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
KeepKeyHDWallet.initialize()constructs the firmware version string from the protobufFeaturesresponse and immediately passes it tosemver.gte()to populate the coin-support feature flags:If any of
majorVersion/minorVersion/patchVersionareundefined(wrong-message-type miscast, decode mismatch, device in an unexpected state, etc.), the code happily produces:…and
semver.gte()then throws the opaque error:This bubbles up through
pairRawDevice()with no actionable diagnostic, which has been observed leaking from the WebUSB pair path in KeepKey Vault on Windows as a silent splash hang on first launch — there's no clue in the engine log that the failure is a malformedFeaturesresponse.Fix
Add a guard in
initialize()that validates the three version fields are numbers before constructingfwVersion. On failure, throw a clear runtime error that names the missing fields:Consumers (KeepKey Vault engine controller, etc.) can now recognize this failure mode and surface a meaningful UI state instead of crashing or hanging.
Tests
New file
packages/hdwallet-keepkey/src/keepkey-initialize.test.tswith 4 tests:majorVersionis missingInvalid Version: vundefined.undefined.undefinedThe tests use a hand-rolled mock transport (
call,getDeviceID,keyring.addAlias,debugLink) — no USB hardware required.Verified locally
yarn build(tsc --build)"Invalid Version: vundefined.undefined.undefined"(exact match for the original bug string)hdwallet-keepkeysuiteyarn lint:ts(tsc --noEmit)Scope
Strictly defensive — no behavioral change for any code path where the device returns a properly populated
Features. The fix only affects the previously-undiagnosablevundefined.undefined.undefinedfailure mode.Test plan
yarn jest --testPathPattern keepkey-initialize)yarn jest --testPathPattern hdwallet-keepkey)yarn lint:ts)