fix: settle a site approval on the port that carries its teardown (closes #275) #289
Reference in New Issue
Block a user
Delete Branch "issue-275-site-approval-race"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #275.
The defect
btn-approvesent the decision withruntime.sendMessage()and closed thewindow on the next line. The close disconnects the approval port, and the
disconnect handler settled a pending SITE approval as a rejection. Those two
events travelled independent channels with nothing ordering them, so whichever
landed first decided the outcome — and driven in a tab the teardown won every
time: the user allowed the connection and the dApp was told they had refused.
The fix
The decision now goes out on the approval port the popup already opens in
show(), which is the same port the close disconnects. One channel is ordered:a message posted on a port is delivered before that port's own disconnect. So
the approval is settled by the decision and the disconnect then finds nothing
pending to reject — whatever the teardown timing is. Nothing is awaited,
nothing is timed, and the popup closes exactly as immediately as before.
windows.onRemovedno longer decides a site approval whose port is connectedeither. In the fallback-window shape that event races the decision on a channel
of its own, which is the same defect one level over; the port disconnect says
the same thing in a defined order, so it is left to say it. A window that
closes before its popup ever connected has nothing else to speak for it and is
still rejected there, so no dApp is left waiting on a window that is gone.
Each approval type now has exactly one close authority: site by port
disconnect, tx/sign by
windows.onRemoved(unchanged — their disconnecthandler still leaves them pending so the user can reopen the toolbar popup).
AUTISTMASK_APPROVAL_RESPONSEis gone. The port name carries the approval id,so the popup no longer names one at all, and the
Unauthorized sendercheckthat message carried moved onto the port (factored into
isExtensionSender(),which the remaining popup-only messages now share). The port disconnect itself
is unchecked exactly as before — a content script that guessed a UUID could
always disconnect one; that is pre-existing and not touched here.
What I established about
chrome.action.openPopup()windows.onRemovedcan never fire for aprompt opened that way, so the port disconnect is the ONLY close signal that
exists on the production path. That is why "leave a site approval pending on
disconnect", the shape tx/sign use, is not available here — it would make
close-without-deciding hang the dApp forever.
present in a tab: dismissing the popup destroys the document exactly as
window.close()does, and the decision and the teardown are emitted back toback either way. Which one arrives first there is NOT measured and is not
measurable from this harness — headless Chromium opens the browser-action
popup but Playwright never exposes it as a page.
property of the port, not on how the document was opened: the decision and
the disconnect are the same channel in both shapes, and the unit tests drive
the
action.openPopup()shape (no window created, port disconnect the onlysignal) as well as the fallback-window shape.
So the "every time" in the issue remains a tab result. Production is not
asserted to have behaved the same way; it is asserted to be unable to behave
differently now.
The harness accommodation is removed
tests/e2e/run.jsno longer patcheswindow.closeon the reserved approvaltab. The two site-prompt tests now drive the shipped decide-then-close in a
real Chromium.
Removing it exposed a harness flake unrelated to this bug:
page.click()resolves only after the renderer acknowledges the action, and a page torn down
by the handler never gets to. It hit
#btn-reject-signand#btn-reject-txtoo — windows that have always closed themselves and were never accommodated —
so it is pre-existing and was simply exposed more often once four more clicks
started self-closing.
clickAndClose()tolerates that one error for the fivebuttons that close their own window and rethrows anything else; the dApp-side
outcome assertion after each call is what proves the click landed.
Demonstrated failing first
Real browser, accommodation removed, unfixed code (
make test-e2e):The user clicked Allow and the page got 4001. Everything after it fails as a
consequence — the origin never became authorized, so no later prompt was raised
at all.
Unit, unfixed code (
make test): the two approve-then-close cases fail and thefour rejection cases pass, which is the defect exactly.
Tests added
tests/backgroundApproval.test.js, all emitting the close IMMEDIATELY afterthe decision with nothing awaited in between:
action.openPopup()shape, no window)connects the site
windows.onRemovedevent toochrome.runtime.onConnectis captured rather than swallowed in the stub, andthe port stub never reorders a decision against the disconnect that follows it.
Verification
Ran, on this branch rebased onto
nextat0be20d7:make fmt, thenmake check— green: 710 tests, 28 suites, prettier clean.make test-e2e— green, 40/40, in the pinned Playwright container.Before the rebase, on the same tree: three consecutive clean
make test-e2eruns green at 37/37. A fourth run that overlapped a concurrent
make checkonthis shared host failed
eth_sendTransaction rejectedwith "the extensionopened no approval window within 30000ms" — a load-sensitive wait on the tx
path, which this change does not touch. Reported rather than hidden.
make test-e2e-firefoxwas NOT run.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.