detached b2fe39a

fix(core): order proxy checks and destructor claims with the wire write

commit
b2fe39a86b7cac0ca5c6a4469678e00a4dc5f2fa
author
brice · · 2026-09-28 07:47 UTC
parent
26d3440
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +101 -34
M client.go +7 -1
@@ -247,6 +247,12 @@ func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{
247247
248248 // SendRequestWithFDs sends a request with file descriptors
249249 func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
250+ return d.sendRequest(objectID, opcode, fds, nil, args)
251+}
252+
253+// sendRequest marshals a request, then runs guard and writes the message
254+// under the send lock. Guard failures and marshaling errors write nothing.
255+func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error {
250256 // Get buffer from pool
251257 buf := bufferPool.Get().(*bytes.Buffer)
252258 defer func() {
@@ -280,7 +286,7 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
280286 copy(data[0:8], header)
281287
282288 // Send message with optional file descriptors
283- return d.sendmsgWithFDs(data, fds)
289+ return d.sendmsgWithFDs(data, fds, guard)
284290 }
285291
286292 // marshalArg marshals a single argument
@@ -247,6 +247,12 @@ func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{
247 247
248 // SendRequestWithFDs sends a request with file descriptors248 // SendRequestWithFDs sends a request with file descriptors
249 func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {249 func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
250+ return d.sendRequest(objectID, opcode, fds, nil, args)
251+}
252+
253+// sendRequest marshals a request, then runs guard and writes the message
254+// under the send lock. Guard failures and marshaling errors write nothing.
255+func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error {
250 // Get buffer from pool256 // Get buffer from pool
251 buf := bufferPool.Get().(*bytes.Buffer)257 buf := bufferPool.Get().(*bytes.Buffer)
252 defer func() {258 defer func() {
@@ -280,7 +286,7 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
280 copy(data[0:8], header)286 copy(data[0:8], header)
281 287
282 // Send message with optional file descriptors288 // Send message with optional file descriptors
283- return d.sendmsgWithFDs(data, fds)289+ return d.sendmsgWithFDs(data, fds, guard)
284 }290 }
285 291
286 // marshalArg marshals a single argument292 // marshalArg marshals a single argument
M core.go +17 -16
@@ -58,20 +58,20 @@ func NewContext(display *Display) *Context {
5858 }
5959 }
6060
61-// SendRequest sends a request through the context
61+// SendRequest sends a request through the context. The proxy is validated
62+// under the connection send lock, so no request reaches the wire after a
63+// destructor for the same proxy.
6264 func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {
63- if err := c.CheckProxy(proxy); err != nil {
64- return err
65- }
66- return c.display.SendRequest(proxy.ID(), uint16(opcode), args...)
65+ return c.SendRequestWithFDs(proxy, opcode, nil, args...)
6766 }
6867
69-// SendRequestWithFDs sends a request with file descriptors through the context
68+// SendRequestWithFDs sends a request with file descriptors through the context.
69+// On error the FDs remain owned by the caller.
7070 func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
7171 if err := c.CheckProxy(proxy); err != nil {
7272 return err
7373 }
74- return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...)
74+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.CheckProxy(proxy) }, args)
7575 }
7676
7777 // CheckProxy rejects stale and foreign proxies before any bytes are written.
@@ -90,21 +90,22 @@ func (c *Context) CheckProxy(proxy Proxy) error {
9090 }
9191
9292 // SendDestructor sends a destructor request exactly once. The proxy is
93-// unregistered atomically before sending, so concurrent callers cannot both
94-// write it; the losers receive an error and nothing reaches the wire.
93+// claimed (unregistered) under the connection send lock, after marshaling
94+// and immediately before the write: concurrent destructors and later requests
95+// on the proxy fail without writing. A marshaling error leaves the proxy
96+// registered. A write error after the claim leaves the proxy unregistered;
97+// such an error means the connection is broken and must be closed.
9598 func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error {
96- if err := c.claimDestroy(proxy); err != nil {
97- return err
98- }
99- return c.display.SendRequest(proxy.ID(), uint16(opcode), args...)
99+ return c.SendDestructorWithFDs(proxy, opcode, nil, args...)
100100 }
101101
102-// SendDestructorWithFDs is SendDestructor for requests carrying FDs.
102+// SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error
103+// the FDs remain owned by the caller.
103104 func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
104- if err := c.claimDestroy(proxy); err != nil {
105+ if err := c.CheckProxy(proxy); err != nil {
105106 return err
106107 }
107- return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...)
108+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.claimDestroy(proxy) }, args)
108109 }
109110
110111 func (c *Context) claimDestroy(proxy Proxy) error {
@@ -58,20 +58,20 @@ func NewContext(display *Display) *Context {
58 }58 }
59 }59 }
60 60
61-// SendRequest sends a request through the context61+// SendRequest sends a request through the context. The proxy is validated
62+// under the connection send lock, so no request reaches the wire after a
63+// destructor for the same proxy.
62 func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {64 func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {
63- if err := c.CheckProxy(proxy); err != nil {65+ return c.SendRequestWithFDs(proxy, opcode, nil, args...)
64- return err
65- }
66- return c.display.SendRequest(proxy.ID(), uint16(opcode), args...)
67 }66 }
68 67
69-// SendRequestWithFDs sends a request with file descriptors through the context68+// SendRequestWithFDs sends a request with file descriptors through the context.
69+// On error the FDs remain owned by the caller.
70 func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {70 func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
71 if err := c.CheckProxy(proxy); err != nil {71 if err := c.CheckProxy(proxy); err != nil {
72 return err72 return err
73 }73 }
74- return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...)74+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.CheckProxy(proxy) }, args)
75 }75 }
76 76
77 // CheckProxy rejects stale and foreign proxies before any bytes are written.77 // CheckProxy rejects stale and foreign proxies before any bytes are written.
@@ -90,21 +90,22 @@ func (c *Context) CheckProxy(proxy Proxy) error {
90 }90 }
91 91
92 // SendDestructor sends a destructor request exactly once. The proxy is92 // SendDestructor sends a destructor request exactly once. The proxy is
93-// unregistered atomically before sending, so concurrent callers cannot both93+// claimed (unregistered) under the connection send lock, after marshaling
94-// write it; the losers receive an error and nothing reaches the wire.94+// and immediately before the write: concurrent destructors and later requests
95+// on the proxy fail without writing. A marshaling error leaves the proxy
96+// registered. A write error after the claim leaves the proxy unregistered;
97+// such an error means the connection is broken and must be closed.
95 func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error {98 func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error {
96- if err := c.claimDestroy(proxy); err != nil {99+ return c.SendDestructorWithFDs(proxy, opcode, nil, args...)
97- return err
98- }
99- return c.display.SendRequest(proxy.ID(), uint16(opcode), args...)
100 }100 }
101 101
102-// SendDestructorWithFDs is SendDestructor for requests carrying FDs.102+// SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error
103+// the FDs remain owned by the caller.
103 func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {104 func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
104- if err := c.claimDestroy(proxy); err != nil {105+ if err := c.CheckProxy(proxy); err != nil {
105 return err106 return err
106 }107 }
107- return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...)108+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.claimDestroy(proxy) }, args)
108 }109 }
109 110
110 func (c *Context) claimDestroy(proxy Proxy) error {111 func (c *Context) claimDestroy(proxy Proxy) error {
M protocol/core/core_test.go +69 -16
@@ -440,34 +440,87 @@ func TestConcurrentDestroySendsOnce(t *testing.T) {
440440 }
441441 defer d.Close()
442442 receiveRequest(t, p)
443- for round := 0; round < 200; round++ {
443+ const rounds, destroyers, committers = 100, 4, 4
444+ for round := 0; round < rounds; round++ {
444445 surface := core.NewSurface(d.Context())
445446 surface.SetID(d.AllocateID())
446447 d.Context().Register(surface)
447- const callers = 8
448448 start := make(chan struct{})
449- results := make(chan error, callers)
450- for i := 0; i < callers; i++ {
451- go func() {
452- <-start
453- results <- surface.Destroy()
454- }()
449+ destroyed := make(chan error, destroyers)
450+ committed := make(chan error, committers)
451+ for i := 0; i < destroyers; i++ {
452+ go func() { <-start; destroyed <- surface.Destroy() }()
453+ }
454+ for i := 0; i < committers; i++ {
455+ go func() { <-start; committed <- surface.Commit() }()
455456 }
456457 close(start)
457- ok := 0
458- for i := 0; i < callers; i++ {
459- if <-results == nil {
460- ok++
458+ okDestroy, okCommit := 0, 0
459+ for i := 0; i < destroyers; i++ {
460+ if <-destroyed == nil {
461+ okDestroy++
462+ }
463+ }
464+ for i := 0; i < committers; i++ {
465+ if <-committed == nil {
466+ okCommit++
461467 }
462468 }
463- if ok != 1 {
464- t.Fatalf("round %d: %d destroys succeeded, want 1", round, ok)
469+ if okDestroy != 1 {
470+ t.Fatalf("round %d: %d destroys succeeded, want 1", round, okDestroy)
471+ }
472+ // Decode every frame: exactly okCommit commits (opcode 6), then one
473+ // destroy (opcode 0) for this surface, and nothing after it.
474+ frames := readFrames(t, p, okCommit+1)
475+ for i, f := range frames {
476+ if f.id != surface.ID() {
477+ t.Fatalf("round %d: frame %d targets object %d", round, i, f.id)
478+ }
479+ last := i == len(frames)-1
480+ if last && f.opcode != 0 || !last && f.opcode != 6 {
481+ t.Fatalf("round %d: frame %d opcode %d (last=%v)", round, i, f.opcode, last)
482+ }
465483 }
466- receiveRequest(t, p)
467484 p.SetReadDeadline(time.Now().Add(2 * time.Millisecond))
468485 if n, err := p.Read(make([]byte, 8)); n != 0 {
469- t.Fatalf("round %d: extra destroy on wire (%d bytes, %v)", round, n, err)
486+ t.Fatalf("round %d: request after destroy (%d bytes, %v)", round, n, err)
470487 }
471488 p.SetReadDeadline(time.Time{})
472489 }
473490 }
491+
492+type wireFrame struct {
493+ id uint32
494+ opcode uint16
495+}
496+
497+func readFrames(t *testing.T, p *net.UnixConn, n int) []wireFrame {
498+ t.Helper()
499+ p.SetReadDeadline(testDeadline())
500+ defer p.SetReadDeadline(time.Time{})
501+ var out []wireFrame
502+ var buf []byte
503+ tmp := make([]byte, 4096)
504+ for len(out) < n {
505+ for len(buf) >= 8 {
506+ size := int(binary.LittleEndian.Uint32(buf[4:8]) >> 16)
507+ if size < 8 || len(buf) < size {
508+ break
509+ }
510+ out = append(out, wireFrame{binary.LittleEndian.Uint32(buf[0:4]), uint16(binary.LittleEndian.Uint32(buf[4:8]))})
511+ buf = buf[size:]
512+ }
513+ if len(out) >= n {
514+ break
515+ }
516+ m, err := p.Read(tmp)
517+ if err != nil {
518+ t.Fatalf("read frames: got %d of %d: %v", len(out), n, err)
519+ }
520+ buf = append(buf, tmp[:m]...)
521+ }
522+ if len(out) != n || len(buf) != 0 {
523+ t.Fatalf("got %d frames (+%d bytes), want %d", len(out), len(buf), n)
524+ }
525+ return out
526+}
@@ -440,34 +440,87 @@ func TestConcurrentDestroySendsOnce(t *testing.T) {
440 }440 }
441 defer d.Close()441 defer d.Close()
442 receiveRequest(t, p)442 receiveRequest(t, p)
443- for round := 0; round < 200; round++ {443+ const rounds, destroyers, committers = 100, 4, 4
444+ for round := 0; round < rounds; round++ {
444 surface := core.NewSurface(d.Context())445 surface := core.NewSurface(d.Context())
445 surface.SetID(d.AllocateID())446 surface.SetID(d.AllocateID())
446 d.Context().Register(surface)447 d.Context().Register(surface)
447- const callers = 8
448 start := make(chan struct{})448 start := make(chan struct{})
449- results := make(chan error, callers)449+ destroyed := make(chan error, destroyers)
450- for i := 0; i < callers; i++ {450+ committed := make(chan error, committers)
451- go func() {451+ for i := 0; i < destroyers; i++ {
452- <-start452+ go func() { <-start; destroyed <- surface.Destroy() }()
453- results <- surface.Destroy()453+ }
454- }()454+ for i := 0; i < committers; i++ {
455+ go func() { <-start; committed <- surface.Commit() }()
455 }456 }
456 close(start)457 close(start)
457- ok := 0458+ okDestroy, okCommit := 0, 0
458- for i := 0; i < callers; i++ {459+ for i := 0; i < destroyers; i++ {
459- if <-results == nil {460+ if <-destroyed == nil {
460- ok++461+ okDestroy++
462+ }
463+ }
464+ for i := 0; i < committers; i++ {
465+ if <-committed == nil {
466+ okCommit++
461 }467 }
462 }468 }
463- if ok != 1 {469+ if okDestroy != 1 {
464- t.Fatalf("round %d: %d destroys succeeded, want 1", round, ok)470+ t.Fatalf("round %d: %d destroys succeeded, want 1", round, okDestroy)
471+ }
472+ // Decode every frame: exactly okCommit commits (opcode 6), then one
473+ // destroy (opcode 0) for this surface, and nothing after it.
474+ frames := readFrames(t, p, okCommit+1)
475+ for i, f := range frames {
476+ if f.id != surface.ID() {
477+ t.Fatalf("round %d: frame %d targets object %d", round, i, f.id)
478+ }
479+ last := i == len(frames)-1
480+ if last && f.opcode != 0 || !last && f.opcode != 6 {
481+ t.Fatalf("round %d: frame %d opcode %d (last=%v)", round, i, f.opcode, last)
482+ }
465 }483 }
466- receiveRequest(t, p)
467 p.SetReadDeadline(time.Now().Add(2 * time.Millisecond))484 p.SetReadDeadline(time.Now().Add(2 * time.Millisecond))
468 if n, err := p.Read(make([]byte, 8)); n != 0 {485 if n, err := p.Read(make([]byte, 8)); n != 0 {
469- t.Fatalf("round %d: extra destroy on wire (%d bytes, %v)", round, n, err)486+ t.Fatalf("round %d: request after destroy (%d bytes, %v)", round, n, err)
470 }487 }
471 p.SetReadDeadline(time.Time{})488 p.SetReadDeadline(time.Time{})
472 }489 }
473 }490 }
491+
492+type wireFrame struct {
493+ id uint32
494+ opcode uint16
495+}
496+
497+func readFrames(t *testing.T, p *net.UnixConn, n int) []wireFrame {
498+ t.Helper()
499+ p.SetReadDeadline(testDeadline())
500+ defer p.SetReadDeadline(time.Time{})
501+ var out []wireFrame
502+ var buf []byte
503+ tmp := make([]byte, 4096)
504+ for len(out) < n {
505+ for len(buf) >= 8 {
506+ size := int(binary.LittleEndian.Uint32(buf[4:8]) >> 16)
507+ if size < 8 || len(buf) < size {
508+ break
509+ }
510+ out = append(out, wireFrame{binary.LittleEndian.Uint32(buf[0:4]), uint16(binary.LittleEndian.Uint32(buf[4:8]))})
511+ buf = buf[size:]
512+ }
513+ if len(out) >= n {
514+ break
515+ }
516+ m, err := p.Read(tmp)
517+ if err != nil {
518+ t.Fatalf("read frames: got %d of %d: %v", len(out), n, err)
519+ }
520+ buf = append(buf, tmp[:m]...)
521+ }
522+ if len(out) != n || len(buf) != 0 {
523+ t.Fatalf("got %d frames (+%d bytes), want %d", len(out), len(buf), n)
524+ }
525+ return out
526+}
M scm_linux.go +8 -1
@@ -134,13 +134,20 @@ func (d *Display) closePendingFDs() {
134134 func CloseSentFD(fd int) error { return unix.Close(fd) }
135135
136136 // sendmsgWithFDs sends a message, attaching file descriptors when present.
137-func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {
137+// A non-nil guard runs under the send lock, so its checks and the write are
138+// ordered atomically with every other request on the connection.
139+func (d *Display) sendmsgWithFDs(buf []byte, fds []int, guard func() error) error {
138140 d.sendMu.Lock()
139141 defer d.sendMu.Unlock()
140142
141143 if d.closed.Load() {
142144 return net.ErrClosed
143145 }
146+ if guard != nil {
147+ if err := guard(); err != nil {
148+ return err
149+ }
150+ }
144151 if len(fds) == 0 {
145152 n, err := d.conn.Write(buf)
146153 if err == nil && n != len(buf) {
@@ -134,13 +134,20 @@ func (d *Display) closePendingFDs() {
134 func CloseSentFD(fd int) error { return unix.Close(fd) }134 func CloseSentFD(fd int) error { return unix.Close(fd) }
135 135
136 // sendmsgWithFDs sends a message, attaching file descriptors when present.136 // sendmsgWithFDs sends a message, attaching file descriptors when present.
137-func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {137+// A non-nil guard runs under the send lock, so its checks and the write are
138+// ordered atomically with every other request on the connection.
139+func (d *Display) sendmsgWithFDs(buf []byte, fds []int, guard func() error) error {
138 d.sendMu.Lock()140 d.sendMu.Lock()
139 defer d.sendMu.Unlock()141 defer d.sendMu.Unlock()
140 142
141 if d.closed.Load() {143 if d.closed.Load() {
142 return net.ErrClosed144 return net.ErrClosed
143 }145 }
146+ if guard != nil {
147+ if err := guard(); err != nil {
148+ return err
149+ }
150+ }
144 if len(fds) == 0 {151 if len(fds) == 0 {
145 n, err := d.conn.Write(buf)152 n, err := d.conn.Write(buf)
146 if err == nil && n != len(buf) {153 if err == nil && n != len(buf) {

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 1

Tags 8

↑↓ moveenter openesc close