Summary
The display preview snapshot writes through a fixed temp path, /tmp/led_matrix_preview.png.tmp. /tmp is world-writable and sticky, and the display service (ledmatrix, User=root) and the web service (ledmatrix-web, User=hdpi) are different users. Once a stale .tmp owned by the other user is left behind, fs.protected_regular makes every subsequent write fail with EACCES — even for root — and nothing ever clears it.
On this rig the preview has been dead for 22.9 hours. The display itself is running fine.
Symptoms
$ journalctl -u ledmatrix | grep Snapshot
17:18:18 WARNING src.display_manager - Snapshot write failing (web preview/health mirror is stale): [Errno 13] Permission denied: '/tmp/led_matrix_preview.png.tmp'
17:23:18 WARNING (same)
17:28:18 WARNING (same)
...every 5 minutes, indefinitely
$ ls -la /tmp/led_matrix_preview*
-rw-rw-r-- 1 root root 10723 Sep 3 18:53 /tmp/led_matrix_preview.png
-rw-rw-r-- 1 hdpi hdpi 92 Sep 4 16:32 /tmp/led_matrix_preview.png.tmp <-- stale, wrong owner
-rw-r--r-- 1 hdpi hdpi 0 Sep 4 17:48 /tmp/led_matrix_preview_viewer
$ ls -ld /tmp
drwxrwxrwt 20 root root 2260 Sep 4 17:48 /tmp
/api/v3/health consequently reports the hardware as stale on a perfectly healthy rig:
"hardware": { "snapshot_age_seconds": 82484.0, "status": "stale" }Root cause — it is not ordinary permissions
Root normally bypasses DAC, so EACCES for root looks impossible. It's fs.protected_regular, which refuses an O_CREAT open of an existing file in a world-writable sticky directory when the file's owner is neither the opener nor the directory owner. Demonstrated directly on the rig:
$ sudo python3 -c "open('/tmp/led_matrix_preview.png.tmp','wb').write(b'x')"
root FAILED: [Errno 13] Permission denied: '/tmp/led_matrix_preview.png.tmp'
So the atomic write-then-rename is safe against torn reads but not against a cross-user stale temp file in a shared sticky directory.
Why it never recovers
display_manager catches the error, logs it (throttled to every 5 minutes) and carries on. Nothing removes or works around the offending file, so the only fix is a human running rm /tmp/led_matrix_preview.png.tmp. The comment at display_manager.py:227 says the failure "is never silent" — true, but "never silent" and "never recovers" are different things, and the log line doesn't say what to delete.
Knock-on effect on the health check
src/common/snapshot_policy.py is explicit that the file's mtime is a liveness proxy:
"The health check ... uses the file's AGE as a liveness proxy: age >= 60s reads as degraded."
"TOUCH_INTERVAL must stay well under it."
TOUCH_INTERVAL is 20s and IDLE_INTERVAL is 30s, so the policy is sound — but both the WRITE and the TOUCH go through the same blocked path, so the liveness proxy inverts: a healthy, rendering display reports stale. Anyone using /api/v3/health for monitoring gets a permanent false alarm.
Suggested fix
Any one of these closes it; the first two are the smallest:
- Unique temp name —
tempfile.mkstemp(dir="/tmp", prefix="led_matrix_preview.", suffix=".png") then os.replace(). A stale file from another user can never collide, because the name is never reused. - Unlink-on-EACCES retry — try to remove the temp path and retry once before giving up. Note this fails too under
protected_regular if the directory isn't owned by the caller, so (1) is more robust. - Move the mirror out of
/tmp — a service-owned directory such as /var/cache/ledmatrix/ (already used for other state) has neither the sticky-bit hazard nor the cross-user ambiguity.
Worth also including the offending path in the warning so the manual fix is obvious:
Snapshot write failing (web preview/health mirror is stale): ...
-> a stale /tmp/led_matrix_preview.png.tmp owned by another user blocks this; remove it
Environment
LEDMatrix v3.3.0-4-g0730d952, Raspberry Pi (Debian trixie, kernel 6.18.34), 256x64 panel.
ledmatrix.service runs as root, ledmatrix-web.service as hdpi. Found while validating plugins — I tried to capture the panel and found the preview 23 hours old.
Summary
The display preview snapshot writes through a fixed temp path,
/tmp/led_matrix_preview.png.tmp./tmpis world-writable and sticky, and the display service (ledmatrix,User=root) and the web service (ledmatrix-web,User=hdpi) are different users. Once a stale.tmpowned by the other user is left behind,fs.protected_regularmakes every subsequent write fail withEACCES— even for root — and nothing ever clears it.On this rig the preview has been dead for 22.9 hours. The display itself is running fine.
Symptoms
/api/v3/healthconsequently reports the hardware as stale on a perfectly healthy rig:Root cause — it is not ordinary permissions
Root normally bypasses DAC, so
EACCESfor root looks impossible. It'sfs.protected_regular, which refuses anO_CREATopen of an existing file in a world-writable sticky directory when the file's owner is neither the opener nor the directory owner. Demonstrated directly on the rig:So the atomic write-then-rename is safe against torn reads but not against a cross-user stale temp file in a shared sticky directory.
Why it never recovers
display_managercatches the error, logs it (throttled to every 5 minutes) and carries on. Nothing removes or works around the offending file, so the only fix is a human runningrm /tmp/led_matrix_preview.png.tmp. The comment atdisplay_manager.py:227says the failure "is never silent" — true, but "never silent" and "never recovers" are different things, and the log line doesn't say what to delete.Knock-on effect on the health check
src/common/snapshot_policy.pyis explicit that the file's mtime is a liveness proxy:TOUCH_INTERVALis 20s andIDLE_INTERVALis 30s, so the policy is sound — but both the WRITE and the TOUCH go through the same blocked path, so the liveness proxy inverts: a healthy, rendering display reportsstale. Anyone using/api/v3/healthfor monitoring gets a permanent false alarm.Suggested fix
Any one of these closes it; the first two are the smallest:
tempfile.mkstemp(dir="/tmp", prefix="led_matrix_preview.", suffix=".png")thenos.replace(). A stale file from another user can never collide, because the name is never reused.protected_regularif the directory isn't owned by the caller, so (1) is more robust./tmp— a service-owned directory such as/var/cache/ledmatrix/(already used for other state) has neither the sticky-bit hazard nor the cross-user ambiguity.Worth also including the offending path in the warning so the manual fix is obvious:
Environment
LEDMatrix
v3.3.0-4-g0730d952, Raspberry Pi (Debian trixie, kernel 6.18.34), 256x64 panel.ledmatrix.serviceruns as root,ledmatrix-web.serviceashdpi. Found while validating plugins — I tried to capture the panel and found the preview 23 hours old.