detached 52935c6

fix(daemon): harden parked lease teardown

commit
52935c6fe4fb23ae5aa207d46e497b3054fdf3de
author
brice · · 2026-08-25 15:46 UTC
parent
b4a3ff1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +38 -9
M internal/usecase/daemon/parked_route.go +15 -9
@@ -24,6 +24,10 @@ type parkedRouteLease struct {
2424 expired bool
2525 }
2626
27+func (lease *parkedRouteLease) matches(id ports.ParkedRouteLeaseID, generation uint64, transport transportSnapshot) bool {
28+ return lease != nil && lease.id == id && lease.generation == generation && lease.transport == transport
29+}
30+
2731 func newParkedRouteLeaseID() (ports.ParkedRouteLeaseID, error) {
2832 var id ports.ParkedRouteLeaseID
2933 if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {
@@ -96,9 +100,7 @@ func (d *Daemon) expireParkedRoute(ac *attachedClient, lease *parkedRouteLease)
96100 return
97101 }
98102 ac.parkedRoute = nil
99- if !lease.suspended {
100- ac.parkedRouteOutput.Store(false)
101- }
103+ ac.parkedRouteOutput.Store(false)
102104 ac.parkedRouteFullPending.Store(false)
103105 ac.parkedRouteMu.Unlock()
104106 if ac.connectionGeneration.Load() == lease.generation && ac.transportSnapshotCurrent(lease.transport) {
@@ -125,7 +127,7 @@ func (ac *attachedClient) parkedRouteStatus(token attachmentConnectionToken, id
125127 ac.parkedRouteMu.Lock()
126128 defer ac.parkedRouteMu.Unlock()
127129 lease := ac.parkedRoute
128- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport {
130+ if !lease.matches(id, token.generation, token.transport) {
129131 return ports.ParkedRouteRejected
130132 }
131133 if !now.Before(lease.expiresAt) {
@@ -148,7 +150,7 @@ func (ac *attachedClient) prepareParkedRoute(token attachmentConnectionToken, id
148150 ac.parkedRouteMu.Lock()
149151 defer ac.parkedRouteMu.Unlock()
150152 lease := ac.parkedRoute
151- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || lease.suspended {
153+ if !lease.matches(id, token.generation, token.transport) || lease.suspended {
152154 return ports.ParkedRouteRejected
153155 }
154156 lease.suspended = true
@@ -163,7 +165,7 @@ func (ac *attachedClient) consumeParkedRoute(token attachmentConnectionToken, id
163165 ac.parkedRouteMu.Lock()
164166 defer ac.parkedRouteMu.Unlock()
165167 lease := ac.parkedRoute
166- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || !lease.suspended {
168+ if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
167169 return ports.ParkedRouteRejected
168170 }
169171 stopParkedRouteLeaseLocked(lease)
@@ -178,12 +180,14 @@ func (ac *attachedClient) beginParkedRouteSwitch(token attachmentConnectionToken
178180 ac.parkedRouteMu.Lock()
179181 defer ac.parkedRouteMu.Unlock()
180182 lease := ac.parkedRoute
181- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || !lease.suspended || lease.inFlight {
183+ if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
182184 return ports.ParkedRouteRejected
183185 }
184186 if lease.expired || !now.Before(lease.expiresAt) {
185187 stopParkedRouteLeaseLocked(lease)
186188 ac.parkedRoute = nil
189+ ac.parkedRouteOutput.Store(false)
190+ ac.parkedRouteFullPending.Store(false)
187191 return ports.ParkedRouteExpired
188192 }
189193 lease.inFlight = true
@@ -197,13 +201,15 @@ func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentConnectionToke
197201 ac.parkedRouteMu.Lock()
198202 defer ac.parkedRouteMu.Unlock()
199203 lease := ac.parkedRoute
200- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || !lease.inFlight {
204+ if !lease.matches(id, token.generation, token.transport) || !lease.inFlight {
201205 return ports.ParkedRouteRejected
202206 }
203207 lease.inFlight = false
204208 if lease.expired || !now.Before(lease.expiresAt) {
205209 stopParkedRouteLeaseLocked(lease)
206210 ac.parkedRoute = nil
211+ ac.parkedRouteOutput.Store(false)
212+ ac.parkedRouteFullPending.Store(false)
207213 return ports.ParkedRouteExpired
208214 }
209215 return 0
@@ -216,7 +222,7 @@ func (ac *attachedClient) consumePublishedParkedRoute(previous, published attach
216222 ac.parkedRouteMu.Lock()
217223 defer ac.parkedRouteMu.Unlock()
218224 lease := ac.parkedRoute
219- if lease == nil || lease.id != id || lease.generation != previous.generation || lease.transport != previous.transport || !lease.suspended || !lease.inFlight {
225+ if !lease.matches(id, previous.generation, previous.transport) || !lease.suspended || !lease.inFlight {
220226 return ports.ParkedRouteRejected
221227 }
222228 // Expiry is checked when the request is admitted. Once publication starts,
@@ -24,6 +24,10 @@ type parkedRouteLease struct {
24 expired bool24 expired bool
25 }25 }
26 26
27+func (lease *parkedRouteLease) matches(id ports.ParkedRouteLeaseID, generation uint64, transport transportSnapshot) bool {
28+ return lease != nil && lease.id == id && lease.generation == generation && lease.transport == transport
29+}
30+
27 func newParkedRouteLeaseID() (ports.ParkedRouteLeaseID, error) {31 func newParkedRouteLeaseID() (ports.ParkedRouteLeaseID, error) {
28 var id ports.ParkedRouteLeaseID32 var id ports.ParkedRouteLeaseID
29 if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {33 if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {
@@ -96,9 +100,7 @@ func (d *Daemon) expireParkedRoute(ac *attachedClient, lease *parkedRouteLease)
96 return100 return
97 }101 }
98 ac.parkedRoute = nil102 ac.parkedRoute = nil
99- if !lease.suspended {103+ ac.parkedRouteOutput.Store(false)
100- ac.parkedRouteOutput.Store(false)
101- }
102 ac.parkedRouteFullPending.Store(false)104 ac.parkedRouteFullPending.Store(false)
103 ac.parkedRouteMu.Unlock()105 ac.parkedRouteMu.Unlock()
104 if ac.connectionGeneration.Load() == lease.generation && ac.transportSnapshotCurrent(lease.transport) {106 if ac.connectionGeneration.Load() == lease.generation && ac.transportSnapshotCurrent(lease.transport) {
@@ -125,7 +127,7 @@ func (ac *attachedClient) parkedRouteStatus(token attachmentConnectionToken, id
125 ac.parkedRouteMu.Lock()127 ac.parkedRouteMu.Lock()
126 defer ac.parkedRouteMu.Unlock()128 defer ac.parkedRouteMu.Unlock()
127 lease := ac.parkedRoute129 lease := ac.parkedRoute
128- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport {130+ if !lease.matches(id, token.generation, token.transport) {
129 return ports.ParkedRouteRejected131 return ports.ParkedRouteRejected
130 }132 }
131 if !now.Before(lease.expiresAt) {133 if !now.Before(lease.expiresAt) {
@@ -148,7 +150,7 @@ func (ac *attachedClient) prepareParkedRoute(token attachmentConnectionToken, id
148 ac.parkedRouteMu.Lock()150 ac.parkedRouteMu.Lock()
149 defer ac.parkedRouteMu.Unlock()151 defer ac.parkedRouteMu.Unlock()
150 lease := ac.parkedRoute152 lease := ac.parkedRoute
151- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || lease.suspended {153+ if !lease.matches(id, token.generation, token.transport) || lease.suspended {
152 return ports.ParkedRouteRejected154 return ports.ParkedRouteRejected
153 }155 }
154 lease.suspended = true156 lease.suspended = true
@@ -163,7 +165,7 @@ func (ac *attachedClient) consumeParkedRoute(token attachmentConnectionToken, id
163 ac.parkedRouteMu.Lock()165 ac.parkedRouteMu.Lock()
164 defer ac.parkedRouteMu.Unlock()166 defer ac.parkedRouteMu.Unlock()
165 lease := ac.parkedRoute167 lease := ac.parkedRoute
166- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || !lease.suspended {168+ if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
167 return ports.ParkedRouteRejected169 return ports.ParkedRouteRejected
168 }170 }
169 stopParkedRouteLeaseLocked(lease)171 stopParkedRouteLeaseLocked(lease)
@@ -178,12 +180,14 @@ func (ac *attachedClient) beginParkedRouteSwitch(token attachmentConnectionToken
178 ac.parkedRouteMu.Lock()180 ac.parkedRouteMu.Lock()
179 defer ac.parkedRouteMu.Unlock()181 defer ac.parkedRouteMu.Unlock()
180 lease := ac.parkedRoute182 lease := ac.parkedRoute
181- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || !lease.suspended || lease.inFlight {183+ if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
182 return ports.ParkedRouteRejected184 return ports.ParkedRouteRejected
183 }185 }
184 if lease.expired || !now.Before(lease.expiresAt) {186 if lease.expired || !now.Before(lease.expiresAt) {
185 stopParkedRouteLeaseLocked(lease)187 stopParkedRouteLeaseLocked(lease)
186 ac.parkedRoute = nil188 ac.parkedRoute = nil
189+ ac.parkedRouteOutput.Store(false)
190+ ac.parkedRouteFullPending.Store(false)
187 return ports.ParkedRouteExpired191 return ports.ParkedRouteExpired
188 }192 }
189 lease.inFlight = true193 lease.inFlight = true
@@ -197,13 +201,15 @@ func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentConnectionToke
197 ac.parkedRouteMu.Lock()201 ac.parkedRouteMu.Lock()
198 defer ac.parkedRouteMu.Unlock()202 defer ac.parkedRouteMu.Unlock()
199 lease := ac.parkedRoute203 lease := ac.parkedRoute
200- if lease == nil || lease.id != id || lease.generation != token.generation || lease.transport != token.transport || !lease.inFlight {204+ if !lease.matches(id, token.generation, token.transport) || !lease.inFlight {
201 return ports.ParkedRouteRejected205 return ports.ParkedRouteRejected
202 }206 }
203 lease.inFlight = false207 lease.inFlight = false
204 if lease.expired || !now.Before(lease.expiresAt) {208 if lease.expired || !now.Before(lease.expiresAt) {
205 stopParkedRouteLeaseLocked(lease)209 stopParkedRouteLeaseLocked(lease)
206 ac.parkedRoute = nil210 ac.parkedRoute = nil
211+ ac.parkedRouteOutput.Store(false)
212+ ac.parkedRouteFullPending.Store(false)
207 return ports.ParkedRouteExpired213 return ports.ParkedRouteExpired
208 }214 }
209 return 0215 return 0
@@ -216,7 +222,7 @@ func (ac *attachedClient) consumePublishedParkedRoute(previous, published attach
216 ac.parkedRouteMu.Lock()222 ac.parkedRouteMu.Lock()
217 defer ac.parkedRouteMu.Unlock()223 defer ac.parkedRouteMu.Unlock()
218 lease := ac.parkedRoute224 lease := ac.parkedRoute
219- if lease == nil || lease.id != id || lease.generation != previous.generation || lease.transport != previous.transport || !lease.suspended || !lease.inFlight {225+ if !lease.matches(id, previous.generation, previous.transport) || !lease.suspended || !lease.inFlight {
220 return ports.ParkedRouteRejected226 return ports.ParkedRouteRejected
221 }227 }
222 // Expiry is checked when the request is admitted. Once publication starts,228 // Expiry is checked when the request is admitted. Once publication starts,
M internal/usecase/daemon/parked_route_test.go +23 -0
@@ -279,6 +279,24 @@ func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) {
279279 require.False(t, ac.parkedRouteOutput.Load())
280280 }
281281
282+func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) {
283+ d, source, ac, sends, releases := newManualTabSession(t, 1)
284+ defer releaseAll(releases)
285+ ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
286+ leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
287+
288+ ac.parkedRouteMu.Lock()
289+ ac.parkedRoute.inFlight = true
290+ ac.parkedRouteMu.Unlock()
291+ status := ac.consumeParkedRoute(source.attachmentToken(ac, ac.transport()), leaseID, d.clock.Now())
292+
293+ require.Equal(t, ports.ParkedRouteRejected, status)
294+ ac.parkedRouteMu.Lock()
295+ require.NotNil(t, ac.parkedRoute)
296+ ac.parkedRouteMu.Unlock()
297+ require.True(t, ac.parkedRouteOutput.Load())
298+}
299+
282300 func TestParkedRouteLeaseExpiryClosesExactRetainedTransport(t *testing.T) {
283301 d, source, ac, _, releases := newManualTabSession(t, 1)
284302 defer releaseAll(releases)
@@ -301,6 +319,11 @@ func TestParkedRouteLeaseExpiryClosesExactRetainedTransport(t *testing.T) {
301319 case <-time.After(time.Second):
302320 t.Fatal("parked transport remained open after its lease expired")
303321 }
322+ ac.parkedRouteMu.Lock()
323+ require.Nil(t, ac.parkedRoute)
324+ ac.parkedRouteMu.Unlock()
325+ require.False(t, ac.parkedRouteOutput.Load())
326+ require.False(t, ac.parkedRouteFullPending.Load())
304327 }
305328
306329 func TestParkedRouteMalformedSwitchRequestsFailClosed(t *testing.T) {
@@ -279,6 +279,24 @@ func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) {
279 require.False(t, ac.parkedRouteOutput.Load())279 require.False(t, ac.parkedRouteOutput.Load())
280 }280 }
281 281
282+func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) {
283+ d, source, ac, sends, releases := newManualTabSession(t, 1)
284+ defer releaseAll(releases)
285+ ac.navigationCapabilities = ports.NavigationCapabilityHomePicker
286+ leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
287+
288+ ac.parkedRouteMu.Lock()
289+ ac.parkedRoute.inFlight = true
290+ ac.parkedRouteMu.Unlock()
291+ status := ac.consumeParkedRoute(source.attachmentToken(ac, ac.transport()), leaseID, d.clock.Now())
292+
293+ require.Equal(t, ports.ParkedRouteRejected, status)
294+ ac.parkedRouteMu.Lock()
295+ require.NotNil(t, ac.parkedRoute)
296+ ac.parkedRouteMu.Unlock()
297+ require.True(t, ac.parkedRouteOutput.Load())
298+}
299+
282 func TestParkedRouteLeaseExpiryClosesExactRetainedTransport(t *testing.T) {300 func TestParkedRouteLeaseExpiryClosesExactRetainedTransport(t *testing.T) {
283 d, source, ac, _, releases := newManualTabSession(t, 1)301 d, source, ac, _, releases := newManualTabSession(t, 1)
284 defer releaseAll(releases)302 defer releaseAll(releases)
@@ -301,6 +319,11 @@ func TestParkedRouteLeaseExpiryClosesExactRetainedTransport(t *testing.T) {
301 case <-time.After(time.Second):319 case <-time.After(time.Second):
302 t.Fatal("parked transport remained open after its lease expired")320 t.Fatal("parked transport remained open after its lease expired")
303 }321 }
322+ ac.parkedRouteMu.Lock()
323+ require.Nil(t, ac.parkedRoute)
324+ ac.parkedRouteMu.Unlock()
325+ require.False(t, ac.parkedRouteOutput.Load())
326+ require.False(t, ac.parkedRouteFullPending.Load())
304 }327 }
305 328
306 func TestParkedRouteMalformedSwitchRequestsFailClosed(t *testing.T) {329 func TestParkedRouteMalformedSwitchRequestsFailClosed(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close