detached 69ad4ea

fix(conn): reject directed signals without a destination

NewSignalTo with an empty destination made Send broadcast the signal. It now fails, and NewSignal stays the broadcast API. The server tests wait on the socket with a per-OS select helper so they build outside Linux, and the async reply test bounds its reads with a socket deadline and requires every reply.
commit
69ad4ea499d2aaeb077f85fcba571d915fd4446f
author
brice · · 2026-10-14 21:01 UTC
parent
12c08ea
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +66 -13
M conn.go +5 -1
@@ -267,9 +267,13 @@ func (c *Conn) NewSignal(path, iface, member, sig string) *Encoder {
267267
268268 // NewSignalTo starts a signal addressed to the single peer dest, a unique
269269 // name such as ":1.42" or a well-known name, and returns the encoder for its
270-// body. Only dest receives it, whatever match rules other peers have.
270+// body. Only dest receives it, whatever match rules other peers have. An
271+// empty dest makes Send fail: use NewSignal to broadcast.
271272 func (c *Conn) NewSignalTo(dest, path, iface, member, sig string) *Encoder {
272273 c.enc.begin(TypeSignal, c.nextSerial(), 0, dest, path, iface, member, "", sig)
274+ if dest == "" {
275+ c.enc.invalid()
276+ }
273277 return &c.enc
274278 }
275279
@@ -267,9 +267,13 @@ func (c *Conn) NewSignal(path, iface, member, sig string) *Encoder {
267 267
268 // NewSignalTo starts a signal addressed to the single peer dest, a unique268 // NewSignalTo starts a signal addressed to the single peer dest, a unique
269 // name such as ":1.42" or a well-known name, and returns the encoder for its269 // name such as ":1.42" or a well-known name, and returns the encoder for its
270-// body. Only dest receives it, whatever match rules other peers have.270+// body. Only dest receives it, whatever match rules other peers have. An
271+// empty dest makes Send fail: use NewSignal to broadcast.
271 func (c *Conn) NewSignalTo(dest, path, iface, member, sig string) *Encoder {272 func (c *Conn) NewSignalTo(dest, path, iface, member, sig string) *Encoder {
272 c.enc.begin(TypeSignal, c.nextSerial(), 0, dest, path, iface, member, "", sig)273 c.enc.begin(TypeSignal, c.nextSerial(), 0, dest, path, iface, member, "", sig)
274+ if dest == "" {
275+ c.enc.invalid()
276+ }
273 return &c.enc277 return &c.enc
274 }278 }
275 279
M server_test.go +13 -12
@@ -3,10 +3,8 @@ package zerobus
33 import (
44 "net"
55 "path/filepath"
6- "syscall"
76 "testing"
87 "time"
9- "unsafe"
108 )
119
1210 // socketPair returns a Conn whose socket is the client end of a Unix socket,
@@ -61,6 +59,11 @@ func TestNewSignalToSetsDestination(t *testing.T) {
6159 if _, err := c.Send(); err == nil {
6260 t.Fatal("invalid destination accepted")
6361 }
62+
63+ c.NewSignalTo("", "/a", "a.b", "Hit", "")
64+ if _, err := c.Send(); err == nil {
65+ t.Fatal("directed signal without destination accepted")
66+ }
6467 }
6568
6669 func TestDirectedSignalReachesOnlyDestination(t *testing.T) {
@@ -225,13 +228,8 @@ func waitReadable(t *testing.T, c *Conn) {
225228 if err := rc.Control(func(f uintptr) { fd = int(f) }); err != nil {
226229 t.Fatal(err)
227230 }
228- var set syscall.FdSet
229- bits := int(unsafe.Sizeof(set.Bits[0])) * 8
230- set.Bits[fd/bits] |= 1 << (uint(fd) % uint(bits))
231- tv := syscall.Timeval{Sec: 5}
232- n, err := syscall.Select(fd+1, &set, nil, nil, &tv)
233- if err != nil || n != 1 {
234- t.Fatalf("select = %d, %v", n, err)
231+ if err := waitFd(fd, 5*time.Second); err != nil {
232+ t.Fatal(err)
235233 }
236234 }
237235
@@ -269,11 +267,14 @@ func TestMethodCallAllocations(t *testing.T) {
269267 }
270268 // The replies must be read for the test to leave the bus quiet; they
271269 // also prove the calls were well formed.
272- deadline := time.Now().Add(5 * time.Second)
273- for got := 0; got < 202 && time.Now().Before(deadline); {
270+ if err := c.c.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
271+ t.Fatal(err)
272+ }
273+ got := 0
274+ for got < 202 {
274275 m, err := c.ReadMessage()
275276 if err != nil {
276- t.Fatal(err)
277+ t.Fatalf("after %d of 202 replies: %v", got, err)
277278 }
278279 if m.Type == TypeMethodReturn && m.ReplySerial <= serial {
279280 got++
@@ -3,10 +3,8 @@ package zerobus
3 import (3 import (
4 "net"4 "net"
5 "path/filepath"5 "path/filepath"
6- "syscall"
7 "testing"6 "testing"
8 "time"7 "time"
9- "unsafe"
10 )8 )
11 9
12 // socketPair returns a Conn whose socket is the client end of a Unix socket,10 // socketPair returns a Conn whose socket is the client end of a Unix socket,
@@ -61,6 +59,11 @@ func TestNewSignalToSetsDestination(t *testing.T) {
61 if _, err := c.Send(); err == nil {59 if _, err := c.Send(); err == nil {
62 t.Fatal("invalid destination accepted")60 t.Fatal("invalid destination accepted")
63 }61 }
62+
63+ c.NewSignalTo("", "/a", "a.b", "Hit", "")
64+ if _, err := c.Send(); err == nil {
65+ t.Fatal("directed signal without destination accepted")
66+ }
64 }67 }
65 68
66 func TestDirectedSignalReachesOnlyDestination(t *testing.T) {69 func TestDirectedSignalReachesOnlyDestination(t *testing.T) {
@@ -225,13 +228,8 @@ func waitReadable(t *testing.T, c *Conn) {
225 if err := rc.Control(func(f uintptr) { fd = int(f) }); err != nil {228 if err := rc.Control(func(f uintptr) { fd = int(f) }); err != nil {
226 t.Fatal(err)229 t.Fatal(err)
227 }230 }
228- var set syscall.FdSet231+ if err := waitFd(fd, 5*time.Second); err != nil {
229- bits := int(unsafe.Sizeof(set.Bits[0])) * 8232+ t.Fatal(err)
230- set.Bits[fd/bits] |= 1 << (uint(fd) % uint(bits))
231- tv := syscall.Timeval{Sec: 5}
232- n, err := syscall.Select(fd+1, &set, nil, nil, &tv)
233- if err != nil || n != 1 {
234- t.Fatalf("select = %d, %v", n, err)
235 }233 }
236 }234 }
237 235
@@ -269,11 +267,14 @@ func TestMethodCallAllocations(t *testing.T) {
269 }267 }
270 // The replies must be read for the test to leave the bus quiet; they268 // The replies must be read for the test to leave the bus quiet; they
271 // also prove the calls were well formed.269 // also prove the calls were well formed.
272- deadline := time.Now().Add(5 * time.Second)270+ if err := c.c.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
273- for got := 0; got < 202 && time.Now().Before(deadline); {271+ t.Fatal(err)
272+ }
273+ got := 0
274+ for got < 202 {
274 m, err := c.ReadMessage()275 m, err := c.ReadMessage()
275 if err != nil {276 if err != nil {
276- t.Fatal(err)277+ t.Fatalf("after %d of 202 replies: %v", got, err)
277 }278 }
278 if m.Type == TypeMethodReturn && m.ReplySerial <= serial {279 if m.Type == TypeMethodReturn && m.ReplySerial <= serial {
279 got++280 got++
A waitfd_linux_test.go +22 -0
@@ -0,0 +1,22 @@
1+package zerobus
2+
3+import (
4+ "fmt"
5+ "syscall"
6+ "time"
7+ "unsafe"
8+)
9+
10+// waitFd blocks until fd is readable or the timeout passes, as a poll loop
11+// does.
12+func waitFd(fd int, timeout time.Duration) error {
13+ var set syscall.FdSet
14+ bits := int(unsafe.Sizeof(set.Bits[0])) * 8
15+ set.Bits[fd/bits] |= 1 << (uint(fd) % uint(bits))
16+ tv := syscall.NsecToTimeval(timeout.Nanoseconds())
17+ n, err := syscall.Select(fd+1, &set, nil, nil, &tv)
18+ if err != nil || n != 1 {
19+ return fmt.Errorf("select = %d, %v", n, err)
20+ }
21+ return nil
22+}
@@ -0,0 +1,22 @@
1+package zerobus
2+
3+import (
4+ "fmt"
5+ "syscall"
6+ "time"
7+ "unsafe"
8+)
9+
10+// waitFd blocks until fd is readable or the timeout passes, as a poll loop
11+// does.
12+func waitFd(fd int, timeout time.Duration) error {
13+ var set syscall.FdSet
14+ bits := int(unsafe.Sizeof(set.Bits[0])) * 8
15+ set.Bits[fd/bits] |= 1 << (uint(fd) % uint(bits))
16+ tv := syscall.NsecToTimeval(timeout.Nanoseconds())
17+ n, err := syscall.Select(fd+1, &set, nil, nil, &tv)
18+ if err != nil || n != 1 {
19+ return fmt.Errorf("select = %d, %v", n, err)
20+ }
21+ return nil
22+}
A waitfd_other_test.go +26 -0
@@ -0,0 +1,26 @@
1+//go:build !linux
2+
3+package zerobus
4+
5+import (
6+ "fmt"
7+ "syscall"
8+ "time"
9+ "unsafe"
10+)
11+
12+// waitFd blocks until fd is readable or the timeout passes, as a poll loop
13+// does. syscall.Select returns only an error outside Linux.
14+func waitFd(fd int, timeout time.Duration) error {
15+ var set syscall.FdSet
16+ bits := int(unsafe.Sizeof(set.Bits[0])) * 8
17+ set.Bits[fd/bits] |= 1 << (uint(fd) % uint(bits))
18+ tv := syscall.NsecToTimeval(timeout.Nanoseconds())
19+ if err := syscall.Select(fd+1, &set, nil, nil, &tv); err != nil {
20+ return fmt.Errorf("select: %v", err)
21+ }
22+ if set.Bits[fd/bits]&(1<<(uint(fd)%uint(bits))) == 0 {
23+ return fmt.Errorf("select: fd %d not readable after %v", fd, timeout)
24+ }
25+ return nil
26+}
@@ -0,0 +1,26 @@
1+//go:build !linux
2+
3+package zerobus
4+
5+import (
6+ "fmt"
7+ "syscall"
8+ "time"
9+ "unsafe"
10+)
11+
12+// waitFd blocks until fd is readable or the timeout passes, as a poll loop
13+// does. syscall.Select returns only an error outside Linux.
14+func waitFd(fd int, timeout time.Duration) error {
15+ var set syscall.FdSet
16+ bits := int(unsafe.Sizeof(set.Bits[0])) * 8
17+ set.Bits[fd/bits] |= 1 << (uint(fd) % uint(bits))
18+ tv := syscall.NsecToTimeval(timeout.Nanoseconds())
19+ if err := syscall.Select(fd+1, &set, nil, nil, &tv); err != nil {
20+ return fmt.Errorf("select: %v", err)
21+ }
22+ if set.Bits[fd/bits]&(1<<(uint(fd)%uint(bits))) == 0 {
23+ return fmt.Errorf("select: fd %d not readable after %v", fd, timeout)
24+ }
25+ return nil
26+}

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 3

↑↓ moveenter openesc close