detached 417c304

fix(remote): validate attachment effects defensively

commit
417c304b8e472375752a478c5bd628aad17bdb16
author
brice · · 2026-08-08 17:59 UTC
parent
87c2338
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +7 -7
M internal/usecase/daemon/client_frame_routing.go +4 -0
@@ -278,6 +278,10 @@ func (d *Daemon) ackOutput(token attachmentConnectionToken, epoch, state uint64)
278278 return false
279279 }
280280 ac.sendMu.Lock()
281+ if ac.output == nil {
282+ ac.sendMu.Unlock()
283+ return false
284+ }
281285 acknowledged := ac.output.ack(epoch, state)
282286 ac.sendMu.Unlock()
283287 if acknowledged {
@@ -278,6 +278,10 @@ func (d *Daemon) ackOutput(token attachmentConnectionToken, epoch, state uint64)
278 return false278 return false
279 }279 }
280 ac.sendMu.Lock()280 ac.sendMu.Lock()
281+ if ac.output == nil {
282+ ac.sendMu.Unlock()
283+ return false
284+ }
281 acknowledged := ac.output.ack(epoch, state)285 acknowledged := ac.output.ack(epoch, state)
282 ac.sendMu.Unlock()286 ac.sendMu.Unlock()
283 if acknowledged {287 if acknowledged {
M internal/usecase/daemon/daemon.go +0 -6
@@ -1154,14 +1154,8 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
11541154 failAttachment()
11551155 return
11561156 }
1157- rc := sess.renderCoordinator()
1158- lease := (*attachmentLease)(nil)
1159- if rc != nil {
1160- lease = rc.attachmentLease(ac)
1161- }
11621157 expected := ac.transportSnapshot()
11631158 welcomeToken := sess.attachmentToken(ac, tr)
1164- welcomeToken.lease = lease
11651159 welcomeTicket, admitted := ac.beginAttachmentEffect(welcomeToken)
11661160 if expected.transport != tr || !admitted || welcomeToken.ac == nil {
11671161 if admitted {
@@ -1154,14 +1154,8 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
1154 failAttachment()1154 failAttachment()
1155 return1155 return
1156 }1156 }
1157- rc := sess.renderCoordinator()
1158- lease := (*attachmentLease)(nil)
1159- if rc != nil {
1160- lease = rc.attachmentLease(ac)
1161- }
1162 expected := ac.transportSnapshot()1157 expected := ac.transportSnapshot()
1163 welcomeToken := sess.attachmentToken(ac, tr)1158 welcomeToken := sess.attachmentToken(ac, tr)
1164- welcomeToken.lease = lease
1165 welcomeTicket, admitted := ac.beginAttachmentEffect(welcomeToken)1159 welcomeTicket, admitted := ac.beginAttachmentEffect(welcomeToken)
1166 if expected.transport != tr || !admitted || welcomeToken.ac == nil {1160 if expected.transport != tr || !admitted || welcomeToken.ac == nil {
1167 if admitted {1161 if admitted {
M internal/usecase/daemon/picker.go +2 -0
@@ -251,6 +251,8 @@ func remotePickerReasonText(reason string) string {
251251 return "session stopped"
252252 case "session_broken":
253253 return "session broken"
254+ case "malformed":
255+ return "catalog malformed"
254256 case "refreshing":
255257 return "refreshing"
256258 case "identity_changed":
@@ -251,6 +251,8 @@ func remotePickerReasonText(reason string) string {
251 return "session stopped"251 return "session stopped"
252 case "session_broken":252 case "session_broken":
253 return "session broken"253 return "session broken"
254+ case "malformed":
255+ return "catalog malformed"
254 case "refreshing":256 case "refreshing":
255 return "refreshing"257 return "refreshing"
256 case "identity_changed":258 case "identity_changed":
M internal/usecase/daemon/remote_picker.go +1 -1
@@ -130,7 +130,7 @@ func remoteSessionOrigin(host string) string {
130130 }
131131
132132 func remoteSessionStateStopped(state string) bool {
133- return state == "down" || state == "stopped"
133+ return state == "down"
134134 }
135135
136136 func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSession, status remoteHostStatus, fetchedAt time.Time) picker.SessionView {
@@ -130,7 +130,7 @@ func remoteSessionOrigin(host string) string {
130 }130 }
131 131
132 func remoteSessionStateStopped(state string) bool {132 func remoteSessionStateStopped(state string) bool {
133- return state == "down" || state == "stopped"133+ return state == "down"
134 }134 }
135 135
136 func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSession, status remoteHostStatus, fetchedAt time.Time) picker.SessionView {136 func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSession, status remoteHostStatus, fetchedAt time.Time) picker.SessionView {

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