From 2a3916ce192d73bdd6f621cc3842782814351227 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 15 Sep 2026 18:09:26 +0200 Subject: fix(notify): cap image dimensions and rank icon_data last A notification is untrusted input. RawImage.PNG computed stride*(Height-1)+Width*Channels in int, so Width=Height=2^31-1 with RowStride=0 wrapped the length expression negative, slipped past the guard, and reached image.NewRGBA, which panics. The D-Bus call path has no recover, so one malformed Notify killed the daemon. Cap Width/Height at 1<<16 before any multiplication and compute the required byte count in int64. image-data, image-path and the deprecated icon_data were read as one tier, with icon_data ahead of image-path, inverting the spec's order. Split the deprecated key into IconDataFromHints and apply tier 1 (image-data/image_data), then image-path, then icon_data. Raw handling is unchanged: encode, hold pendingImage, WriteImage after Add, SetImage. --- internal/notify/service_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'internal/notify/service_test.go') diff --git a/internal/notify/service_test.go b/internal/notify/service_test.go index 3d7c2cc..f7d7195 100644 --- a/internal/notify/service_test.go +++ b/internal/notify/service_test.go @@ -133,6 +133,46 @@ func TestNotifyMaterialisesImageData(t *testing.T) { } } +// The deprecated icon_data must lose to the image-path URI it outranked when +// the keys were read in one tier. +func TestNotifyIconDataDoesNotBeatImagePath(t *testing.T) { + var live []Popup + s := &Service{dir: t.TempDir(), timers: map[uint32]*time.Timer{}} + s.store = NewStore(s.emitClosed, func(l, _ []Popup) { live = l }, s.removeImage) + rgba := []byte{255, 0, 0, 255, 0, 255, 0, 255} + hints := map[string]dbus.Variant{ + "icon_data": dbus.MakeVariant([]interface{}{int32(2), int32(1), int32(8), true, int32(8), int32(4), rgba}), + "image-path": dbus.MakeVariant("/tmp/shot.png"), + } + if _, dbusErr := s.Notify("t", 0, "", "s", "b", nil, hints, -1); dbusErr != nil { + t.Fatal(dbusErr) + } + if len(live) != 1 || live[0].Image != "/tmp/shot.png" { + t.Fatalf("published %+v, want image-path", live) + } +} + +// image-data is tier 1, above both image-path and icon_data. +func TestNotifyImageDataBeatsIconDataAndPath(t *testing.T) { + var live []Popup + s := &Service{dir: t.TempDir(), timers: map[uint32]*time.Timer{}} + s.store = NewStore(s.emitClosed, func(l, _ []Popup) { live = l }, s.removeImage) + rgba := []byte{255, 0, 0, 255, 0, 255, 0, 255} + hints := map[string]dbus.Variant{ + "image-data": dbus.MakeVariant([]interface{}{int32(2), int32(1), int32(8), true, int32(8), int32(4), rgba}), + "icon_data": dbus.MakeVariant([]interface{}{int32(9), int32(1), int32(8), true, int32(8), int32(4), rgba}), + "image-path": dbus.MakeVariant("/tmp/shot.png"), + } + id, dbusErr := s.Notify("t", 0, "", "s", "b", nil, hints, -1) + if dbusErr != nil { + t.Fatal(dbusErr) + } + want := filepath.Join(ImagesDir(s.dir), strconv.FormatUint(uint64(id), 10)+".png") + if len(live) != 1 || live[0].Image != want { + t.Fatalf("published %+v, want image-data %q", live, want) + } +} + func TestNotifyReturnsAnIDAndPublishes(t *testing.T) { conn := busOrSkip(t) dir := t.TempDir() -- cgit v1.2.3