config.Config has a dozen setters and no lock #97

Open
opened 2026-08-18 20:37:45 +00:00 by yonlu · 0 comments
Owner

config.Config has no synchronisation, and roughly a dozen Set* methods that read-modify-write shared state and then Save() to disk. Every one is a bound method the frontend can call.

Salvaged from docs/dev/config-suggestions.md, which is deleted. It is the only one of that file's eight suggestions that survived — the rest were overtaken by rewrites: applyDefaults exists (#6), scan concurrency is configurable as ScanConcurrency with SSD/HDD detection (#5), httphandler.go and the WriteHeader-after-render bug it described are gone entirely (#4, #3), and paths go through backend/system (#9). What is left of #7 (a config file watcher) and #8 (structured validation errors) is speculative and is not carried forward.

The shape of it

SetLibraryDirectory, SetScanConcurrency, SetDownloadPreferences, SetThemeAccentColor, SetThemeBackgroundShade, SetDefaultPage, SetQueueFallback, SetAllowMeteredCatalogDownload, SetTrackListColumns — each mutates the struct and calls Save(), and Load() can run against the same fields. Nothing holds a lock.

Wails does not serialise binding calls onto one goroutine, so two settings changed in quick succession are two goroutines in the same struct. The realistic damage is a lost write — two Set* calls interleaving so the TOML written last is missing the other's change — rather than memory corruption, because the fields are independent.

Why nothing has caught it

make test runs with -race across three build configurations and is clean, because no test drives two setters concurrently. A race detector only reports races that actually happen. This is the same shape as TestNoWritesOnTheReadPool: a fault that a single-threaded test cannot see by construction.

What to do

An sync.RWMutex on Config, taken for write in every Set* and Load/Save, and for read in every Get*. Worth checking while there whether Save() should hold the lock across the file write or copy under lock and write outside it — the write is disk I/O and the getters are called from render paths.

A test that hammers two setters concurrently under -race is what turns this from an argument into a fact, and should be written first, so it fails before the mutex and passes after.

Priority

Low. It is a real defect and it is not one anybody has hit — settings are changed by a human clicking, one at a time.

`config.Config` has no synchronisation, and roughly a dozen `Set*` methods that read-modify-write shared state and then `Save()` to disk. Every one is a bound method the frontend can call. Salvaged from `docs/dev/config-suggestions.md`, which is deleted. **It is the only one of that file's eight suggestions that survived** — the rest were overtaken by rewrites: `applyDefaults` exists (#6), scan concurrency is configurable as `ScanConcurrency` with SSD/HDD detection (#5), `httphandler.go` and the `WriteHeader`-after-render bug it described are gone entirely (#4, #3), and paths go through `backend/system` (#9). What is left of #7 (a config file watcher) and #8 (structured validation errors) is speculative and is not carried forward. ## The shape of it `SetLibraryDirectory`, `SetScanConcurrency`, `SetDownloadPreferences`, `SetThemeAccentColor`, `SetThemeBackgroundShade`, `SetDefaultPage`, `SetQueueFallback`, `SetAllowMeteredCatalogDownload`, `SetTrackListColumns` — each mutates the struct and calls `Save()`, and `Load()` can run against the same fields. Nothing holds a lock. Wails does not serialise binding calls onto one goroutine, so two settings changed in quick succession are two goroutines in the same struct. The realistic damage is a lost write — two `Set*` calls interleaving so the TOML written last is missing the other's change — rather than memory corruption, because the fields are independent. ## Why nothing has caught it `make test` runs with `-race` across three build configurations and is clean, because no test drives two setters concurrently. A race detector only reports races that actually happen. This is the same shape as `TestNoWritesOnTheReadPool`: a fault that a single-threaded test cannot see by construction. ## What to do An `sync.RWMutex` on `Config`, taken for write in every `Set*` and `Load`/`Save`, and for read in every `Get*`. Worth checking while there whether `Save()` should hold the lock across the file write or copy under lock and write outside it — the write is disk I/O and the getters are called from render paths. A test that hammers two setters concurrently under `-race` is what turns this from an argument into a fact, and should be written **first**, so it fails before the mutex and passes after. ## Priority Low. It is a real defect and it is not one anybody has hit — settings are changed by a human clicking, one at a time.
yonlu added the Platform/Desktop
Priority
Low
4
Kind/BugPlatform/AndroidArea/Settings
labels 2026-08-18 20:37:45 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#97