detached b45de7b

Merge pull request #5 from bnema/fix/abandon-proxy

feat(context): add Abandon for compositor-destroyed objects
commit
b45de7b0a0bfec376d12eef9a4195f005b92c8c2
author
bnema · · 2026-10-01 17:58 UTC
committer
GitHub · · 2026-10-01 17:58 UTC
merge of
9be920f + 2f95df7 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.6.1
2 files · +51 -0
M core.go +9 -0
@@ -212,6 +212,15 @@ func (c *Context) claimDestroy(proxy Proxy) error {
212212 return nil
213213 }
214214
215+// Abandon stops delivering events to proxy without sending any request. Use
216+// it for objects the compositor destroys itself, such as a wl_callback whose
217+// done event may still be in flight: the object stays a zombie, so a late
218+// event is discarded and the compositor's wl_display.delete_id retires the ID.
219+// It returns an error when proxy is not registered on this context.
220+func (c *Context) Abandon(proxy Proxy) error {
221+ return c.claimDestroy(proxy)
222+}
223+
215224 // zombie stands in for an object the client destroyed until the compositor
216225 // acknowledges the destruction with wl_display.delete_id. Events that were in
217226 // flight for it are discarded; their descriptors are closed.
@@ -212,6 +212,15 @@ func (c *Context) claimDestroy(proxy Proxy) error {
212 return nil212 return nil
213 }213 }
214 214
215+// Abandon stops delivering events to proxy without sending any request. Use
216+// it for objects the compositor destroys itself, such as a wl_callback whose
217+// done event may still be in flight: the object stays a zombie, so a late
218+// event is discarded and the compositor's wl_display.delete_id retires the ID.
219+// It returns an error when proxy is not registered on this context.
220+func (c *Context) Abandon(proxy Proxy) error {
221+ return c.claimDestroy(proxy)
222+}
223+
215 // zombie stands in for an object the client destroyed until the compositor224 // zombie stands in for an object the client destroyed until the compositor
216 // acknowledges the destruction with wl_display.delete_id. Events that were in225 // acknowledges the destruction with wl_display.delete_id. Events that were in
217 // flight for it are discarded; their descriptors are closed.226 // flight for it are discarded; their descriptors are closed.
M hardening_test.go +42 -0
@@ -125,6 +125,48 @@ func TestDestroyedObjectEventsAreDroppedUntilDeleteID(t *testing.T) {
125125 }
126126 }
127127
128+// An abandoned object (a wl_callback the compositor destroys itself) sends no
129+// request: a late event is dropped and the compositor's delete_id retires the
130+// ID, both without a protocol error.
131+func TestAbandonedObjectAcceptsLateEventAndServerDeleteID(t *testing.T) {
132+ client, peer := unixSocketPair(t)
133+ d := newDisplay(client)
134+ p := &fdSigProxy{BaseProxy: BaseProxy{id: d.AllocateID(), context: d.context}}
135+ d.context.Register(p)
136+ if err := d.context.Abandon(p); err != nil {
137+ t.Fatal(err)
138+ }
139+ if err := d.context.Abandon(p); err == nil {
140+ t.Fatal("second Abandon succeeded")
141+ }
142+ if err := d.context.SendRequest(p, 0); err == nil {
143+ t.Fatal("request on abandoned proxy succeeded")
144+ }
145+ if err := peer.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
146+ t.Fatal(err)
147+ }
148+ if n, _ := peer.Read(make([]byte, 64)); n != 0 {
149+ t.Fatalf("Abandon sent %d bytes", n)
150+ }
151+
152+ sendMessageWithFDs(t, peer, message(p.ID(), 0, []byte{1, 0, 0, 0}), nil)
153+ if err := d.Dispatch(); err != nil {
154+ t.Fatalf("late event for abandoned object: %v", err)
155+ }
156+ if p.calls != 0 {
157+ t.Fatalf("abandoned proxy received %d events", p.calls)
158+ }
159+ body := make([]byte, 4)
160+ binary.LittleEndian.PutUint32(body, p.ID())
161+ sendMessageWithFDs(t, peer, message(displayObjectID, 1, body), nil)
162+ if err := d.Dispatch(); err != nil {
163+ t.Fatalf("delete_id for abandoned object: %v", err)
164+ }
165+ if got := d.AllocateID(); got != p.ID() {
166+ t.Fatalf("ID %d not reused after delete_id (got %d)", p.ID(), got)
167+ }
168+}
169+
128170 // A plain Go int is ambiguous (value or descriptor) and must be rejected
129171 // instead of being written as zero.
130172 func TestMarshalRejectsPlainInt(t *testing.T) {
@@ -125,6 +125,48 @@ func TestDestroyedObjectEventsAreDroppedUntilDeleteID(t *testing.T) {
125 }125 }
126 }126 }
127 127
128+// An abandoned object (a wl_callback the compositor destroys itself) sends no
129+// request: a late event is dropped and the compositor's delete_id retires the
130+// ID, both without a protocol error.
131+func TestAbandonedObjectAcceptsLateEventAndServerDeleteID(t *testing.T) {
132+ client, peer := unixSocketPair(t)
133+ d := newDisplay(client)
134+ p := &fdSigProxy{BaseProxy: BaseProxy{id: d.AllocateID(), context: d.context}}
135+ d.context.Register(p)
136+ if err := d.context.Abandon(p); err != nil {
137+ t.Fatal(err)
138+ }
139+ if err := d.context.Abandon(p); err == nil {
140+ t.Fatal("second Abandon succeeded")
141+ }
142+ if err := d.context.SendRequest(p, 0); err == nil {
143+ t.Fatal("request on abandoned proxy succeeded")
144+ }
145+ if err := peer.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
146+ t.Fatal(err)
147+ }
148+ if n, _ := peer.Read(make([]byte, 64)); n != 0 {
149+ t.Fatalf("Abandon sent %d bytes", n)
150+ }
151+
152+ sendMessageWithFDs(t, peer, message(p.ID(), 0, []byte{1, 0, 0, 0}), nil)
153+ if err := d.Dispatch(); err != nil {
154+ t.Fatalf("late event for abandoned object: %v", err)
155+ }
156+ if p.calls != 0 {
157+ t.Fatalf("abandoned proxy received %d events", p.calls)
158+ }
159+ body := make([]byte, 4)
160+ binary.LittleEndian.PutUint32(body, p.ID())
161+ sendMessageWithFDs(t, peer, message(displayObjectID, 1, body), nil)
162+ if err := d.Dispatch(); err != nil {
163+ t.Fatalf("delete_id for abandoned object: %v", err)
164+ }
165+ if got := d.AllocateID(); got != p.ID() {
166+ t.Fatalf("ID %d not reused after delete_id (got %d)", p.ID(), got)
167+ }
168+}
169+
128 // A plain Go int is ambiguous (value or descriptor) and must be rejected170 // A plain Go int is ambiguous (value or descriptor) and must be rejected
129 // instead of being written as zero.171 // instead of being written as zero.
130 func TestMarshalRejectsPlainInt(t *testing.T) {172 func TestMarshalRejectsPlainInt(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 1

Tags 8

↑↓ moveenter openesc close