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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 267 | 267 | ||
| 268 | 268 | // NewSignalTo starts a signal addressed to the single peer dest, a unique | |
| 269 | 269 | // 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 | 272 | func (c *Conn) NewSignalTo(dest, path, iface, member, sig string) *Encoder { | |
| 272 | 273 | c.enc.begin(TypeSignal, c.nextSerial(), 0, dest, path, iface, member, "", sig) | |
| 274 | + | if dest == "" { | |
| 275 | + | c.enc.invalid() | |
| 276 | + | } | |
| 273 | 277 | return &c.enc | |
| 274 | 278 | } | |
| 275 | 279 | ||
| @@ -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 unique | 268 | // 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 its | 269 | // 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.enc | 277 | return &c.enc | ||
| 274 | } | 278 | } | ||
| 275 | 279 | ||||
M server_test.go +13 -12
| @@ -3,10 +3,8 @@ package zerobus | |||
| 3 | 3 | import ( | |
| 4 | 4 | "net" | |
| 5 | 5 | "path/filepath" | |
| 6 | - | "syscall" | |
| 7 | 6 | "testing" | |
| 8 | 7 | "time" | |
| 9 | - | "unsafe" | |
| 10 | 8 | ) | |
| 11 | 9 | ||
| 12 | 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 | 59 | if _, err := c.Send(); err == nil { | |
| 62 | 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 | 69 | func TestDirectedSignalReachesOnlyDestination(t *testing.T) { | |
| @@ -225,13 +228,8 @@ func waitReadable(t *testing.T, c *Conn) { | |||
| 225 | 228 | if err := rc.Control(func(f uintptr) { fd = int(f) }); err != nil { | |
| 226 | 229 | t.Fatal(err) | |
| 227 | 230 | } | |
| 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) | |
| 235 | 233 | } | |
| 236 | 234 | } | |
| 237 | 235 | ||
| @@ -269,11 +267,14 @@ func TestMethodCallAllocations(t *testing.T) { | |||
| 269 | 267 | } | |
| 270 | 268 | // The replies must be read for the test to leave the bus quiet; they | |
| 271 | 269 | // 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 { | |
| 274 | 275 | m, err := c.ReadMessage() | |
| 275 | 276 | if err != nil { | |
| 276 | - | t.Fatal(err) | |
| 277 | + | t.Fatalf("after %d of 202 replies: %v", got, err) | |
| 277 | 278 | } | |
| 278 | 279 | if m.Type == TypeMethodReturn && m.ReplySerial <= serial { | |
| 279 | 280 | 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.FdSet | 231 | + | if err := waitFd(fd, 5*time.Second); err != nil { |
| 229 | - | bits := int(unsafe.Sizeof(set.Bits[0])) * 8 | 232 | + | 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; they | 268 | // 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 | + | } | |||