Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

- Use the Makefile: `make run`, `make build`, `make test`, `make fmt`, `make vet`; `make help` lists packaging/security targets.
- Match CI before handoff: `make fmt-check` (`goimports -local github.com/frathe/picfetch`), `go vet ./...`, `go build ./...`, then `go test -timeout 20m -race ./...` from the repository root.
- **HEIC decoder pin:** `go.mod` replaces `github.com/gen2brain/heic` with a fork commit containing [gen2brain/heic#16](https://github.com/gen2brain/heic/pull/16) (fixes native memory leak [issue #15](https://github.com/gen2brain/heic/issues/15)). Remove the `replace` and bump to an official release once upstream tags a version that includes that fix. Optional manual RSS check: `PICFETCH_HEIC_LEAK_TEST=1 go test -tags=heicleak -run TestHEICDecode_DoesNotGrowRSSUnbounded ./internal/imaging/...`.
- Run focused tests while iterating, e.g. `go test -run TestE2E -v ./internal/ui/...`; the complete suite remains the final check.
- Golden screenshots are under `internal/ui/testdata/`. Regenerate only with `make golden` (Docker linux/amd64), inspect `internal/ui/testdata/failed/*.png`, and never commit failed renders.
- Packaging uses Fyne/Fyne-cross; macOS is native, while Windows/Linux cross-builds require Docker. `fyne package` may bump `FyneApp.toml`’s build number.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ require (
golang.org/x/mod v0.40.0
)

replace github.com/gen2brain/heic => github.com/frathe/heic v0.0.0-20260820164529-0ac0a39f8206

require (
fyne.io/systray v1.12.2 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frathe/heic v0.0.0-20260820164529-0ac0a39f8206 h1:+0q4uGHAATWnRF/IGETH9q5+yVi2/HPwq0O8b0/wFy4=
github.com/frathe/heic v0.0.0-20260820164529-0ac0a39f8206/go.mod h1:ja42wMJc4fpnKsfdUJxeZa2YqqRnes1wS0xqs5+8o5w=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
Expand All @@ -119,8 +121,6 @@ github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/gen2brain/avif v0.6.0 h1:/8WSgcU+IEF0jhKYsUZ/mzlziFuTeJFpIKBj2siTQps=
github.com/gen2brain/avif v0.6.0/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/heic v0.7.1 h1:Aha1sZdKEeZeWl5o0xkSg7NBRhhkrlokGVCRri+2Qcc=
github.com/gen2brain/heic v0.7.1/go.mod h1:ja42wMJc4fpnKsfdUJxeZa2YqqRnes1wS0xqs5+8o5w=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
Expand Down
68 changes: 68 additions & 0 deletions internal/imaging/heic_leak_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//go:build heicleak

package imaging

import (
"os"
"runtime"
"runtime/debug"
"testing"

"fyne.io/fyne/v2/storage"
)

func TestHEICDecode_DoesNotGrowRSSUnbounded(t *testing.T) {
if os.Getenv("PICFETCH_HEIC_LEAK_TEST") == "" {
t.Skip("set PICFETCH_HEIC_LEAK_TEST=1 to run native RSS check")
}

data, err := os.ReadFile("testdata/test_exif.heic")
if err != nil {
t.Fatalf("read fixture: %v", err)
}
path := writeTempFile(t, "leak.heic", data)

// Warm libheif/wazero once so one-time init is not counted as per-decode growth.
if _, err := LoadImage(storage.NewFileURI(path), DefaultImgCacheBytes); err != nil {
t.Fatalf("warmup decode: %v", err)
}
settleRSS()

const mid = 20
for i := 0; i < mid; i++ {
if _, err := LoadImage(storage.NewFileURI(path), DefaultImgCacheBytes); err != nil {
t.Fatalf("decode %d: %v", i, err)
}
}
settleRSS()
rssMid, ok := readRSS()
if !ok {
t.Skip("RSS measurement unavailable on this platform")
}

const iterations = 20
for i := 0; i < iterations; i++ {
if _, err := LoadImage(storage.NewFileURI(path), DefaultImgCacheBytes); err != nil {
t.Fatalf("decode %d: %v", mid+i, err)
}
}
settleRSS()
rssAfter, ok := readRSS()
if !ok {
t.Fatal("RSS measurement failed after decode loop")
}

// With the upstream leak, RSS climbs roughly linearly with decode count.
// After PR #16 the second batch should not add much beyond wasm init noise.
const maxGrowthMB = 80
if growth := rssAfter - rssMid; growth > maxGrowthMB*1024*1024 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsigned subtraction wraps on RSS decrease causing false failure

Medium Severity

growth := rssAfter - rssMid operates on uint64 values. If RSS legitimately decreases between the two measurement points (e.g., the OS reclaims pages, or settleRSS releases memory from the first batch), the subtraction wraps around to a value near math.MaxUint64, which far exceeds maxGrowthMB*1024*1024. This causes a false test failure reporting "leak suspected" even when there is no leak. A signed comparison or an early-return when rssAfter <= rssMid would prevent the wrap.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ac75135. Configure here.

t.Fatalf("RSS grew %d bytes over %d decodes after warmup; want <= %d MB (leak suspected)", growth, iterations, maxGrowthMB)
}
}

func settleRSS() {
debug.FreeOSMemory()
runtime.GC()
debug.FreeOSMemory()
runtime.GC()
}
30 changes: 30 additions & 0 deletions internal/imaging/rss_heicleak_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build heicleak && linux

package imaging

import (
"os"
"strconv"
"strings"
)

func readRSS() (uint64, bool) {
data, err := os.ReadFile("/proc/self/status")
if err != nil {
return 0, false
}
for line := range strings.SplitSeq(string(data), "\n") {
if after, ok := strings.CutPrefix(line, "VmRSS:"); ok {
fields := strings.Fields(after)
if len(fields) == 0 {
return 0, false
}
kb, err := strconv.ParseUint(fields[0], 10, 64)
if err != nil {
return 0, false
}
return kb * 1024, true
}
}
return 0, false
}
7 changes: 7 additions & 0 deletions internal/imaging/rss_heicleak_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build heicleak && !linux

package imaging

func readRSS() (uint64, bool) {
return 0, false
}
2 changes: 2 additions & 0 deletions needs_refactoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ dependency, and one upstream library bug.
- **Fix**: watch for the upstream release containing PR #16 and bump. If it
stalls, add a `replace` to the patched fork — one line, immediately
shippable. Either way, add a note in `AGENTS.md` so the pin isn't forgotten.
- **Mitigation (2026-08-26):** `go.mod` replace → `frathe/heic@0ac0a39` until
upstream releases PR #16. Remove replace on bump.

### 2. The duplicate-visibility model lives inside the grid feature

Expand Down
Loading