detached ef8c520

feat(server): add Client.Credentials for peer PID, UID and GID

commit
ef8c520d36c9e462928b1740044251691041825a
author
brice · · 2026-09-30 09:02 UTC
committer
brice · · 2026-09-30 14:46 UTC
parent
d4696de
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +64 -9
M server/display.go +29 -7
@@ -9,6 +9,7 @@ import (
99 "sync/atomic"
1010 "unsafe"
1111
12+ "github.com/bnema/purego"
1213 "golang.org/x/sys/unix"
1314 )
1415
@@ -234,16 +235,37 @@ func (d *Display) Stopped() <-chan struct{} { return d.stopped }
234235 // LiveResources reports resources not yet destroyed. Display goroutine only.
235236 func LiveResources() int { return len(live.resources) }
236237
237-// PID is the process ID of the client, read from the socket credentials
238-// (SO_PEERCRED) when it connected. Zero for an invalid client.
239-// Display goroutine only.
240-func (c Client) PID() int {
238+// Credentials are the peer credentials of a client, read from the socket
239+// (SO_PEERCRED) when it connected.
240+type Credentials struct {
241+ PID int
242+ UID uint32
243+ GID uint32
244+}
245+
246+var credScratch [3]int32
247+
248+var errInvalidClient = errors.New("purego-libwayland: invalid client")
249+
250+// Credentials returns the peer credentials of the client. It returns an error
251+// for an invalid (zero) Client. It does not allocate. Display goroutine only.
252+func (c Client) Credentials() (Credentials, error) {
241253 if c.c == 0 {
254+ return Credentials{}, errInvalidClient
255+ }
256+ // Package-level scratch (display goroutine only) avoids heap-escaping locals.
257+ purego.Syscall6(symClientGetCredentials, c.c, uintptr(unsafe.Pointer(&credScratch[0])), uintptr(unsafe.Pointer(&credScratch[1])), uintptr(unsafe.Pointer(&credScratch[2])), 0, 0)
258+ return Credentials{PID: int(credScratch[0]), UID: uint32(credScratch[1]), GID: uint32(credScratch[2])}, nil
259+}
260+
261+// PID is the process ID of the client, from Credentials. Zero for an invalid
262+// client. Display goroutine only.
263+func (c Client) PID() int {
264+ cr, err := c.Credentials()
265+ if err != nil {
242266 return 0
243267 }
244- var pid, uid, gid int32
245- wlClientGetCredentials(c.c, unsafe.Pointer(&pid), unsafe.Pointer(&uid), unsafe.Pointer(&gid))
246- return int(pid)
268+ return cr.PID
247269 }
248270
249271 func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {
@@ -9,6 +9,7 @@ import (
9 "sync/atomic"9 "sync/atomic"
10 "unsafe"10 "unsafe"
11 11
12+ "github.com/bnema/purego"
12 "golang.org/x/sys/unix"13 "golang.org/x/sys/unix"
13 )14 )
14 15
@@ -234,16 +235,37 @@ func (d *Display) Stopped() <-chan struct{} { return d.stopped }
234 // LiveResources reports resources not yet destroyed. Display goroutine only.235 // LiveResources reports resources not yet destroyed. Display goroutine only.
235 func LiveResources() int { return len(live.resources) }236 func LiveResources() int { return len(live.resources) }
236 237
237-// PID is the process ID of the client, read from the socket credentials238+// Credentials are the peer credentials of a client, read from the socket
238-// (SO_PEERCRED) when it connected. Zero for an invalid client.239+// (SO_PEERCRED) when it connected.
239-// Display goroutine only.240+type Credentials struct {
240-func (c Client) PID() int {241+ PID int
242+ UID uint32
243+ GID uint32
244+}
245+
246+var credScratch [3]int32
247+
248+var errInvalidClient = errors.New("purego-libwayland: invalid client")
249+
250+// Credentials returns the peer credentials of the client. It returns an error
251+// for an invalid (zero) Client. It does not allocate. Display goroutine only.
252+func (c Client) Credentials() (Credentials, error) {
241 if c.c == 0 {253 if c.c == 0 {
254+ return Credentials{}, errInvalidClient
255+ }
256+ // Package-level scratch (display goroutine only) avoids heap-escaping locals.
257+ purego.Syscall6(symClientGetCredentials, c.c, uintptr(unsafe.Pointer(&credScratch[0])), uintptr(unsafe.Pointer(&credScratch[1])), uintptr(unsafe.Pointer(&credScratch[2])), 0, 0)
258+ return Credentials{PID: int(credScratch[0]), UID: uint32(credScratch[1]), GID: uint32(credScratch[2])}, nil
259+}
260+
261+// PID is the process ID of the client, from Credentials. Zero for an invalid
262+// client. Display goroutine only.
263+func (c Client) PID() int {
264+ cr, err := c.Credentials()
265+ if err != nil {
242 return 0266 return 0
243 }267 }
244- var pid, uid, gid int32268+ return cr.PID
245- wlClientGetCredentials(c.c, unsafe.Pointer(&pid), unsafe.Pointer(&uid), unsafe.Pointer(&gid))
246- return int(pid)
247 }269 }
248 270
249 func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {271 func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {
M server/hotpath_test.go +33 -0
@@ -2,6 +2,7 @@ package server
22
33 import (
44 "context"
5+ "os"
56 "testing"
67 "unsafe"
78
@@ -134,3 +135,35 @@ func BenchmarkPostEvent(b *testing.B) {
134135 b.Fatal("display stopped")
135136 }
136137 }
138+
139+func TestClientCredentials(t *testing.T) {
140+ iface := &Interface{Name: "cred_test", Version: 1, Requests: []Message{{Name: "request", Signature: "uuu"}}, Events: []Message{{Name: "event", Signature: "uuu"}}}
141+ d, r := hotDisplay(t, iface, hotHandler)
142+ var (
143+ got, zero Credentials
144+ err, zerr error
145+ allocs float64
146+ pid int
147+ )
148+ c := r.Client()
149+ if !d.Do(func() {
150+ got, err = c.Credentials()
151+ zero, zerr = Client{}.Credentials()
152+ pid = c.PID()
153+ allocs = testing.AllocsPerRun(100, func() { _, _ = c.Credentials() })
154+ }) {
155+ t.Fatal("display stopped")
156+ }
157+ if err != nil {
158+ t.Fatal(err)
159+ }
160+ if got.PID != os.Getpid() || got.UID != uint32(os.Getuid()) || got.GID != uint32(os.Getgid()) || pid != got.PID {
161+ t.Fatalf("credentials %+v pid %d, want pid=%d uid=%d gid=%d", got, pid, os.Getpid(), os.Getuid(), os.Getgid())
162+ }
163+ if zerr == nil || zero != (Credentials{}) || (Client{}).PID() != 0 {
164+ t.Fatalf("invalid client: %+v, %v", zero, zerr)
165+ }
166+ if allocs != 0 {
167+ t.Fatalf("Credentials allocs = %g, want 0", allocs)
168+ }
169+}
@@ -2,6 +2,7 @@ package server
2 2
3 import (3 import (
4 "context"4 "context"
5+ "os"
5 "testing"6 "testing"
6 "unsafe"7 "unsafe"
7 8
@@ -134,3 +135,35 @@ func BenchmarkPostEvent(b *testing.B) {
134 b.Fatal("display stopped")135 b.Fatal("display stopped")
135 }136 }
136 }137 }
138+
139+func TestClientCredentials(t *testing.T) {
140+ iface := &Interface{Name: "cred_test", Version: 1, Requests: []Message{{Name: "request", Signature: "uuu"}}, Events: []Message{{Name: "event", Signature: "uuu"}}}
141+ d, r := hotDisplay(t, iface, hotHandler)
142+ var (
143+ got, zero Credentials
144+ err, zerr error
145+ allocs float64
146+ pid int
147+ )
148+ c := r.Client()
149+ if !d.Do(func() {
150+ got, err = c.Credentials()
151+ zero, zerr = Client{}.Credentials()
152+ pid = c.PID()
153+ allocs = testing.AllocsPerRun(100, func() { _, _ = c.Credentials() })
154+ }) {
155+ t.Fatal("display stopped")
156+ }
157+ if err != nil {
158+ t.Fatal(err)
159+ }
160+ if got.PID != os.Getpid() || got.UID != uint32(os.Getuid()) || got.GID != uint32(os.Getgid()) || pid != got.PID {
161+ t.Fatalf("credentials %+v pid %d, want pid=%d uid=%d gid=%d", got, pid, os.Getpid(), os.Getuid(), os.Getgid())
162+ }
163+ if zerr == nil || zero != (Credentials{}) || (Client{}).PID() != 0 {
164+ t.Fatalf("invalid client: %+v, %v", zero, zerr)
165+ }
166+ if allocs != 0 {
167+ t.Fatalf("Credentials allocs = %g, want 0", allocs)
168+ }
169+}
M server/lib.go +2 -2
@@ -22,7 +22,6 @@ var (
2222 wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr
2323 wlGlobalRemove func(global uintptr)
2424 wlResourcePostError func(resource uintptr, code uint32, msg uintptr)
25- wlClientGetCredentials func(client uintptr, pid, uid, gid unsafe.Pointer)
2625 )
2726
2827 var (
@@ -31,6 +30,7 @@ var (
3130
3231 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr
3332 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr
33+ symClientGetCredentials uintptr
3434 symErrnoLocation uintptr
3535 // wl_fixes support (libwayland 1.26+); zero on older libraries.
3636 symFixesAckGlobalRemove uintptr
@@ -67,7 +67,6 @@ func load() error {
6767 reg(&wlGlobalCreate, "wl_global_create")
6868 reg(&wlGlobalRemove, "wl_global_remove")
6969 reg(&wlResourcePostError, "wl_resource_post_error")
70- reg(&wlClientGetCredentials, "wl_client_get_credentials")
7170 for _, entry := range []struct {
7271 name string
7372 sym *uintptr
@@ -76,6 +75,7 @@ func load() error {
7675 {"wl_event_loop_dispatch", &symEventLoopDispatch},
7776 {"wl_resource_create", &symResourceCreate},
7877 {"wl_resource_get_id", &symResourceGetID},
78+ {"wl_client_get_credentials", &symClientGetCredentials},
7979 {"wl_resource_set_dispatcher", &symResourceSetDispatcher},
8080 {"wl_resource_post_event_array", &symResourcePostEventArr},
8181 {"wl_resource_destroy", &symResourceDestroy},
@@ -22,7 +22,6 @@ var (
22 wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr22 wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr
23 wlGlobalRemove func(global uintptr)23 wlGlobalRemove func(global uintptr)
24 wlResourcePostError func(resource uintptr, code uint32, msg uintptr)24 wlResourcePostError func(resource uintptr, code uint32, msg uintptr)
25- wlClientGetCredentials func(client uintptr, pid, uid, gid unsafe.Pointer)
26 )25 )
27 26
28 var (27 var (
@@ -31,6 +30,7 @@ var (
31 30
32 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr31 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr
33 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr32 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr
33+ symClientGetCredentials uintptr
34 symErrnoLocation uintptr34 symErrnoLocation uintptr
35 // wl_fixes support (libwayland 1.26+); zero on older libraries.35 // wl_fixes support (libwayland 1.26+); zero on older libraries.
36 symFixesAckGlobalRemove uintptr36 symFixesAckGlobalRemove uintptr
@@ -67,7 +67,6 @@ func load() error {
67 reg(&wlGlobalCreate, "wl_global_create")67 reg(&wlGlobalCreate, "wl_global_create")
68 reg(&wlGlobalRemove, "wl_global_remove")68 reg(&wlGlobalRemove, "wl_global_remove")
69 reg(&wlResourcePostError, "wl_resource_post_error")69 reg(&wlResourcePostError, "wl_resource_post_error")
70- reg(&wlClientGetCredentials, "wl_client_get_credentials")
71 for _, entry := range []struct {70 for _, entry := range []struct {
72 name string71 name string
73 sym *uintptr72 sym *uintptr
@@ -76,6 +75,7 @@ func load() error {
76 {"wl_event_loop_dispatch", &symEventLoopDispatch},75 {"wl_event_loop_dispatch", &symEventLoopDispatch},
77 {"wl_resource_create", &symResourceCreate},76 {"wl_resource_create", &symResourceCreate},
78 {"wl_resource_get_id", &symResourceGetID},77 {"wl_resource_get_id", &symResourceGetID},
78+ {"wl_client_get_credentials", &symClientGetCredentials},
79 {"wl_resource_set_dispatcher", &symResourceSetDispatcher},79 {"wl_resource_set_dispatcher", &symResourceSetDispatcher},
80 {"wl_resource_post_event_array", &symResourcePostEventArr},80 {"wl_resource_post_event_array", &symResourcePostEventArr},
81 {"wl_resource_destroy", &symResourceDestroy},81 {"wl_resource_destroy", &symResourceDestroy},

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 14

↑↓ moveenter openesc close