A closed connection still decodes frames, resurrecting _state #115
Labels
No labels
area:defold
area:docs
area:infra
area:protocol
area:testing
kind:bug
kind:chore
phase:breadth
phase:core
phase:hardening
phase:slice
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
yuris/spacetimedb-defold#115
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
A connection the consumer has closed still decodes frames, and decoding one
mutates state the SDK has already declared final. Surfaced by review of #113,
deliberately not folded into it.
Reproduction
Reachable on HTML5, on the path #113 documents.
CloseConnection(
/tmp/verbatim/websocket/src/websocket.cpp:165-186in extension-websocket4.2.4) gates its entire body on
STATE_CONNECTED, and the unconditionalSetState(conn, STATE_DISCONNECTED)is compiled only underHAVE_WSLAY. So onHTML5 a
close()during the handshake is a genuine no-op: the socket is neverclosed, the browser still fires
onopen, and the server's frames still arrive.Nothing on this side stops them being decoded.
stdb/init.lua:526feedsEVENT_MESSAGEstraight intoconn:handle_frame, and neitherM.handle_frame(
stdb/connection.lua:1420) norM.dispatch(:1394) checks whether theconnection is closed.
What one decoded frame does to a closed connection
credentials(stdb/connection.lua:965-991) runs on anyInitialConnectionorIdentityTokenand unconditionally:c._state = "connected"(:972), on a connectionwhose
closedflag is true:981-989), writing identity andtoken back to the save slot after the consumer asked to be done
"connected"(:990), soon_eventand the builder'son_connectfire again post-closeSubscription and transaction handlers refill the cache on the same path.
Why it is worth fixing rather than documenting
M.state's own docstring (stdb/connection.lua:1428-1434) asserts the propertythat is violated:
and
schedule_reconnect's terminal branch (stdb/init.lua:578-580) asserts iteven more explicitly:
Both are false today.
_stateis written"closed"atinit.lua:581and:654, andcredentialswrites"connected"over it.This already cost a bug. #113's send guard needs two conjuncts —
not closing and conn._state ~= "closed"— precisely because_stateis notsticky. A review pass recommended collapsing it to the
_statetest alone,verified
closing ⟹ _state == "closed"at the instant ofclose(), and missedthat something moves it back; the collapse reintroduced a silent strand (no
frame, no callback, no error,
is_pending()true forever) and had to bereverted. The non-stickiness is a trap that has already been fallen into once.
Suggested fix
A closed guard in
M.dispatch— drop the frame, or emitunhandled, ratherthan mutating state. That makes
_stategenuinely sticky and lets #113'sguards retire their second conjunct.
A smaller variant, if the broader fix is unwanted: have
schedule_reconnect's terminal branch setclosing = trueas well as_state = "closed", since it already claims exactly that meaning. One stickytest then replaces both conjuncts. This does not fix the re-persist, the
re-emit, or the cache refill.
Either way the docstrings at
connection.lua:1428-1434andinit.lua:578-580should match whatever ends up true.
Notes
close()path:CloseConnectiontransitions toSTATE_DISCONNECTEDthere, so noEVENT_CONNECTEDor later frame arrives.reconnect = falsedrop, no transport path delivers a frameeither —
OnUpdateerases and destroys the connection in the same iterationit delivers
EVENT_DISCONNECTED(websocket.cpp:845-863), and theframe_budgetqueue is cleared first. Only the HTML5close()path isreachable in a real program; a consumer hand-calling
db:dispatch(...)canreach it anywhere, which is what
dev/test/test_init.lua's zombie test doesdeliberately.
A regression test for the defect itself belongs with the fix.