Summary
With Jaws.AutoSession enabled, a successful WebSocket upgrade can publish a new Session before attaching its Request. A concurrent caller can obtain that Session through Jaws.Sessions() and close it during this gap. The upgrade then attaches the already-closed Session to the Request.
This violates the lifecycle ordering around Session.Close:
- If association wins first,
Close must see the Request, detach it, and queue its reload.
- If
Close wins first, the Request must not subsequently associate with that Session.
Supported public interleaving
- Construct
Jaws, enable AutoSession before serving, and start Serve.
- Create and claim a Request through
NewRequest and UseRequest.
- Begin a valid WebSocket upgrade through
Request.ServeHTTP.
- Once the auto-created Session becomes visible through
Jaws.Sessions(), concurrently call Session.Close.
- Allow the upgrade to continue.
A ResponseWriter wrapper whose Header method waits after observing Jaws.Sessions() makes the interleaving deterministic. The observed result is:
after Close: session_count=0 max_age=-1 requests=0
ConnectFn: same_session=true session_count=0 max_age=-1 requests=1
handshake Set-Cookie="autosession=<id>; Path=/; HttpOnly; SameSite=Lax"
dial_error=<nil>
The handshake succeeds, but its Request points at an expired Session absent from the active-session registry. Because Close saw no associated Requests, the Request also misses the required reload.
Root cause
ensureAutoSession in request.go calls newSession before attaching the returned Session. newSession in session.go inserts the Session into jw.sessions, then invokes response/request cookie operations before returning. Jaws.Sessions therefore exposes it while its Request list is empty. After Session.Close deletes and expires it, ensureAutoSession attaches it without checking whether it is dead.
Suggested fix
Give AutoSession a creation path that establishes the initiating Request's membership before publishing the Session. Serialize that transition with Session.Close under the established lock order, release all internal locks before calling ResponseWriter, and ensure the emitted cookie reflects whichever transition wins.
Acceptance criteria
Related issues
Summary
With
Jaws.AutoSessionenabled, a successful WebSocket upgrade can publish a new Session before attaching its Request. A concurrent caller can obtain that Session throughJaws.Sessions()and close it during this gap. The upgrade then attaches the already-closed Session to the Request.This violates the lifecycle ordering around
Session.Close:Closemust see the Request, detach it, and queue its reload.Closewins first, the Request must not subsequently associate with that Session.Supported public interleaving
Jaws, enableAutoSessionbefore serving, and startServe.NewRequestandUseRequest.Request.ServeHTTP.Jaws.Sessions(), concurrently callSession.Close.A
ResponseWriterwrapper whoseHeadermethod waits after observingJaws.Sessions()makes the interleaving deterministic. The observed result is:The handshake succeeds, but its Request points at an expired Session absent from the active-session registry. Because
Closesaw no associated Requests, the Request also misses the required reload.Root cause
ensureAutoSessioninrequest.gocallsnewSessionbefore attaching the returned Session.newSessioninsession.goinserts the Session intojw.sessions, then invokes response/request cookie operations before returning.Jaws.Sessionstherefore exposes it while its Request list is empty. AfterSession.Closedeletes and expires it,ensureAutoSessionattaches it without checking whether it is dead.Suggested fix
Give AutoSession a creation path that establishes the initiating Request's membership before publishing the Session. Serialize that transition with
Session.Closeunder the established lock order, release all internal locks before callingResponseWriter, and ensure the emitted cookie reflects whichever transition wins.Acceptance criteria
Closereturns, the closed Session is never subsequently installed inRequest.Sessionor appended toSession.Requests.Closedetaches the Request and queues exactly one reload, including before WebSocket subscription.Closewins, the handshake does not emit a live cookie for the closed Session.ResponseWriter.Headercalls remain deadlock-free.go test -race.Related issues
Closeruns; here publication precedes association, soClosecannot see that Request.