Skip to content

[Security] Keep screen-recording state out of world-writable /tmp - #8374

Merged
ErikMelton merged 6 commits into
omacom:quattrofrom
Chessing234:security/screenrecord-tmp-state
Sep 15, 2026
Merged

ErikMelton merged 6 commits into
omacom:quattrofrom
Chessing234:security/screenrecord-tmp-state

Conversation

@Chessing234

@Chessing234 Chessing234 commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8372.

Recording state, region state, and the optional recorder debug log now use the per-user runtime directory. Without XDG_RUNTIME_DIR, they use ${XDG_STATE_HOME:-$HOME/.local/state}/omarchy; the webcam resizer uses that same fallback so region anchoring remains consistent.

The fallback directory is explicitly mode 0700, including when it already exists with broader permissions. This protects recording paths, geometry and recorder stderr even when XDG_STATE_HOME is traversable by other users. Directory creation and permission repair happen only on recording start, so an unusable directory does not prevent the stop path from signalling an active recorder.

Both troubleshooting documents now give the runtime and fallback debug-log locations.

Validation

All 26 assertions in test/shell.d/screenrecording-test.sh pass, including actual start-path runs with stubbed recorder/compositor commands, runtime and fallback paths, webcam region anchoring, and an existing 0755 fallback repaired to 0700. Tests ran on a local macOS copy with only Bash shebangs adjusted to the installed Bash 5 interpreter; native Arch recording and GUI validation remain outstanding. Bash syntax and git diff checks pass.

Copilot AI balanced review requested due to automatic review settings August 26, 2026 11:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Moves screen-recording state and diagnostics into the per-user runtime directory to prevent /tmp path manipulation.

Changes:

  • Relocates recording state, region, and debug files.
  • Updates diagnostic documentation.
  • Adds regression coverage for recording state placement.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated no comments.

File Description
bin/omarchy-capture-screenrecording Moves runtime files out of fixed /tmp paths.
test/shell.d/screenrecording-test.sh Tests secure state-file placement.
default/agents/skills/omarchy/capture.md Updates debug-log location.
default/agents/skills/omarchy/contributing.md Updates troubleshooting guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed at 8fd7576, with an independent second opinion from Codex at xhigh reasoning. The change is correct and complete for what it claims: I grepped every reader and writer of omarchy-screenrecord-filename, omarchy-screenrecord.log, omarchy-screenrecord-region and every hardcoded /tmp/omarchy-screenrecord* across bin/, shell/, default/, migrations/, docs/ and manual/, and nothing else reads the old location. The bar indicator (shell/plugins/bar/indicators/ScreenRecording.qml:18) and the Stop Screenrecording menu guard (default/omarchy/omarchy-menu.jsonc:60) both go through pgrep -f '^gpu-screen-recorder', so the indicator and the stop path are unaffected. Both stop-path reads (lines 230 and 263) use RECORDING_FILE, and REGION_FILE resolves to the same place it did before, matching bin/omarchy-capture-webcam-resize:11. Codex agreed on all of that; its independence is not currently guaranteed, so treat the agreement as agreement rather than as a separate confirmation.

Tests ran on a disposable VM, not on the review machine. ./test/cli passes, and ./test/shell.d/screenrecording-test.sh passes 21 assertions. I also checked the new assertions have teeth by mutation: reverting line 45 to the old /tmp name fails screen recording keeps no state under a fixed /tmp name, as your description says.

Pushed one commit (72080c5), test-only:

That first assertion also failed on the fixed script whenever /tmp/omarchy-screenrecord-filename already existed — which is not hypothetical. A recording interrupted before the upgrade leaves one behind, and the mutation run above leaves one behind too, so the very next run of the suite red-lights for the wrong reason (I hit this on the worker). Worse, the file another local account creates is one you cannot delete, so the scenario this PR is about would permanently break the test that proves it fixed. The commit snapshots the name's mtime and size before the run and compares after, instead of demanding /tmp be bare. The unfixed script rewrites the file either way, so the regression is still caught — verified both directions on the worker, with a stale file present.

Two things left for the maintainer to decide, not defects I would change under you:

  • RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}" (line 44) puts both files back at fixed, pre-creatable names in world-writable /tmp when the variable is unset. Under a normal uwsm/SDDM login pam_systemd always sets it, so this is narrow; but it is the same shape as the defect being closed. It matches the dominant idiom in the repo (16 sites, including the REGION_FILE line you deliberately lined up with), which is a real argument for leaving it. It does not match your own [Security] Stage diagnostics where only their owner can read them #8370, which falls back to $XDG_STATE_HOME/omarchy and pre-creates at mode 600 for the same class of problem. Whichever way it goes, the two PRs should probably agree.
  • A recording already running when omarchy update lands wrote its filename to the old /tmp path; the new stop path reads the runtime dir, finds nothing, and skips finalization — the MP4 survives in the recordings directory unfinalized, but the toast opens an empty path. Codex raised this and suggested a legacy bridge. I do not think one should be added: reading the old /tmp name whenever the runtime file is absent would restore exactly the attacker-controlled read this PR removes, and absent is the normal state before any recording. A clean cutover looks like the right call, but it is worth the maintainer knowing the transition exists. Separately, the stop path's behaviour when RECORDING_FILE is missing (empty filename, ffmpeg -i "", a "saved" toast that opens nothing) is a pre-existing rough edge, not something this PR introduces — out of scope here.

Related, and it matters for merge order: this closes #8372. #7532 adds test/shell.d/screenrecording-portal-fallback-test.sh with a hardcoded RECORDING_FILE="/tmp/omarchy-screenrecord-filename" — that test will fail once this merges, whichever lands second. #7484 hardcodes a new /tmp/omarchy-screenrecord-paused in the script, the menu JSON and the QML indicator, citing "matching the existing /tmp/omarchy-screenrecord-filename convention" — a convention this PR removes. The competing pause/resume PR #6470 already uses ${XDG_RUNTIME_DIR:-/tmp} and needs nothing.

Waiting on the maintainer.

@itz4blitz itz4blitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The direction is right and the test design is a standout — comparing the /tmp name across the run instead of demanding absence, so a leftover from a pre-fix recording can't false-positive, is the correct call.

One substantive problem: the fallback re-opens the hole this PR exists to close.

RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}"
RECORDING_FILE="$RUNTIME_DIR/omarchy-screenrecord-filename"

When XDG_RUNTIME_DIR is unset — a recording started from a systemd user unit without the import, a cron/at job, an ssh session before systemd --user, or a stale env after logout — the state file lands right back at the predictable /tmp/omarchy-screenrecord-filename the PR's own comment calls out: "a name in world-writable /tmp is a name any other local account can create first and then point wherever it likes." The comment and the code disagree exactly in the branch where the attack still works.

Since the stop path reads the file back and feeds the path to ffmpeg (writes beside it), mv (replaces it) and rm -f (deletes its preview), a pre-planted symlink at the fallback name is a same-user arbitrary-file clobber or delete on stop — the same primitive #8372 describes.

Suggested resolution, matching the resolution #8370 and #8429 use:

RUNTIME_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}"

…and mkdir -p "$RUNTIME_DIR" before use, or refuse to run when neither is usable. REGION_FILE carried the same :-/tmp fallback before this PR, so this is a chance to fix both rather than preserve one.

A test for the unset branch would pin it: run the start path with env -u XDG_RUNTIME_DIR and a fake HOME, then assert no omarchy-screenrecord-filename appears at a /tmp path and the state file lands under the state dir instead. Happy to contribute that if useful.

Everything else — the debug-log relocation, the docs updates, the stop-path assertions — checks out.

@Chessing234

Copy link
Copy Markdown
Contributor Author

fell back to the state dir when there is no runtime dir, instead of /tmp.

@omarchybot

Copy link
Copy Markdown
Collaborator

Re-reviewed the new commit (a47bfff), with a second opinion from Codex at xhigh reasoning. The fallback is the right answer to the point raised last time, but the commit landed two integration problems, both now fixed on your branch.

The suite is red on any clean machine. ./test/shell.d/screenrecording-test.sh aborts at line 341 before the first assertion in that block: the file runs under set -euo pipefail, and tmp_state_before=$(stat -c '%y %s' "$tmp_state" 2>/dev/null) takes the script down when /tmp/omarchy-screenrecord-filename is absent — which is the normal case, and now the case on every machine, because the fixed script no longer creates it. On a fresh worker the file reports 18 passed, 0 failed, exit 1, so the three assertions you added never ran at all. This is my fault, not yours: it came in with the snapshot commit I pushed on the last pass, and it survived review only because the mutation runs that produced it had left the name behind on the machine I tested on. a8717fd adds || true to both snapshots. Codex found the same thing statically and traced it to the same line.

bin/omarchy-capture-webcam-resize:11 was left behind. It reads ${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region; the recorder now writes that region file to ${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}. Nothing changes when XDG_RUNTIME_DIR is set, but in exactly the case the new fallback exists for, a region recording with a webcam writes the region to one path and reads it from another: the overlay anchors to the whole monitor instead of the captured region, and the reader is still looking at a /tmp name anyone can plant. 94f59e8b gives the resizer the same expression and adds an assertion that a recording started without a runtime dir still anchors the camera to its region. Codex flagged this independently from a pinned copy of your tree; that is the one finding here I would call a genuine second pair of eyes rather than agreement.

Both fixes are mutation-checked on a disposable VM, not on the review machine: reverting the || true puts the suite back to 18 passed, exit 1, and reverting the resizer's path fails the new anchor assertion (24 passed, 1 failed). With both in, ./test/shell.d/screenrecording-test.sh is 25 passed, 0 failed, bin-style-test.sh passes, and ./test/cli exits 0.

Two smaller things left for you, neither of them blocking:

  • mkdir -p "$RUNTIME_DIR" || exit 1 (line 47) runs before the script decides whether this is a start or a stop. default/hypr/bindings/utilities.lua:38 binds Alt+Print to omarchy-capture-screenrecording --stop-recording || omarchy-menu toggle trigger.capture.screenrecord, so a failed mkdir is indistinguishable from "nothing was recording" and opens the record menu while a recording is still running. It needs a broken $HOME or a non-directory at the runtime path to fire, so it is close to unreachable — but creating the directory inside start_screenrecording, where the writes actually happen, costs nothing and keeps the stop path unconditional. Codex raised the ordering; I checked the keybinding and it is as described.
  • The body still says the state moves "next to the region file under $XDG_RUNTIME_DIR (0700, per-user)" and lists three assertions. That is no longer the change: there is a state-directory fallback now, and the region file's reader only just caught up. Worth a rewrite before this is read at merge time.

One thing I checked and am not reporting, because it does not hold: the fallback directory is created 0755 under the default umask 022, and the files in it 0644, which does not match the 0700 the comment above it opens with. On a stock Omarchy install it makes no difference — /etc/login.defs sets HOME_MODE 0700 and $HOME measures 700 on a machine built from the ISO, so nothing under ~/.local/state is reachable by another local account whatever its own mode is. Codex rated it low on the assumption that the ancestors are searchable; they are not.

Related, unchanged from the last pass and still worth the maintainer's attention for merge order: this closes #8372 in full, including the debug log the issue names. #7532 hardcodes RECORDING_FILE="/tmp/omarchy-screenrecord-filename" in a new test file and #7484 hardcodes a new /tmp/omarchy-screenrecord-paused across the script, the menu JSON and the QML indicator, explicitly citing the convention this removes; #6470 needs nothing. #8430 does the same class of fix for the Battle.net installer log with a third shape again — mktemp under TMPDIR at 0600 — so there are now three different answers to "not a fixed name in /tmp" in flight at once.

Codex's independence is not currently guaranteed, so read its agreement as agreement. On this pass it earned more than that twice: it found the set -e abort by static tracing before my worker run reproduced it, and it found the resizer path from a pinned export of your tree that my own edits could not reach. Its first run stopped on its own when it noticed the worktree had moved under it mid-review, which is why it was re-run pinned.

Waiting on you for the two items above, then on the maintainer.

@Chessing234

Copy link
Copy Markdown
Contributor Author

@dhh @ryanrhughes ready for review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The fallback directory may expose sensitive debug data, and debug-log relocation needs coverage.

Review details

Suppressed comments (2)

default/agents/skills/omarchy/capture.md:37

  • The implementation now falls back to the Omarchy state directory when XDG_RUNTIME_DIR is unset, so this instruction points affected users to a nonexistent path. Mention the fallback location as well.
If recording fails to start, rerun with `OMARCHY_SCREENRECORD_DEBUG=true` to
collect a log at `$XDG_RUNTIME_DIR/omarchy-screenrecord.log` worth attaching to a bug
report.

default/agents/skills/omarchy/contributing.md:41

  • The no-runtime-directory fallback writes this log to the Omarchy state directory, not this path. Include that fallback so the troubleshooting procedure remains valid in the case the code explicitly supports.
`OMARCHY_SCREENRECORD_DEBUG=true` and attach `$XDG_RUNTIME_DIR/omarchy-screenrecord.log`.
  • Files reviewed: 3/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@ErikMelton

Copy link
Copy Markdown
Member
  • [P2] Protect files created in the fallback directory. mkdir -p and the subsequent paths use ordinary umask permissions. Without XDG_RUNTIME_DIR, a traversable XDG_STATE_HOME can leave the directory 0755 and the state/debug files 0644, exposing recording paths, geometry, and recorder stderr to other users. The fallback directory or individual artifacts should be explicitly private (0700/0600).

  • [P2] Document the fallback debug-log location. Both capture.md and contributing.md always direct users to $XDG_RUNTIME_DIR/omarchy-screenrecord.log. When that variable is unset, the implementation instead writes under ${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/, so the troubleshooting instructions point to the wrong file.

@Chessing234

Copy link
Copy Markdown
Contributor Author

@ErikMelton Addressed both findings in ce438cd: existing fallback directories are explicitly made 0700, and both docs include the fallback debug-log path. Directory creation also moved to recording start so it cannot block the stop path. All 26 focused assertions pass with Bash 5 compatibility on macOS; native Arch/GUI validation is still outstanding. Could you re-review the update?

Chessing234 and others added 6 commits September 12, 2026 12:25
The path of the recording in flight was kept at the fixed name
/tmp/omarchy-screenrecord-filename, two lines above a region file that
already used the per-user runtime directory. Stopping a recording reads
that file back and uses its contents as a path: ffmpeg writes a
"-processed.mp4" beside it, `mv` puts that over it, `rm -f` deletes its
"-preview.png", and the saved toast offers to open it. Any other local
account can create that name first, since /tmp's sticky bit stops
deletion but not creation, and point all of it wherever it likes.

Move it, and the debug log, next to the region file under
$XDG_RUNTIME_DIR.
The assertion failed on the fixed script whenever that name already existed -- a
recording interrupted before the upgrade leaves one, and another local account
can create one you cannot delete, which is the finding itself. Comparing the
name's mtime and size across the run keeps the regression caught (the unfixed
script rewrites it either way) without depending on global state no test can
control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
:-/tmp put the recording filename and region files back at the same
fixed /tmp names this PR exists to leave.
The file is expected to be absent on a clean machine, but the suite runs
under `set -euo pipefail`, where a command substitution that fails takes
the script down with it -- so `stat` on the missing name aborted the run
before the first assertion in this block. It survived review only because
the mutation runs that produced it had left the name behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The recorder's fallback moved to the state directory; the overlay resizer
still read `${XDG_RUNTIME_DIR:-/tmp}`, so on a session without a runtime
dir the region was written to one path and read from another and the
camera anchored to the whole monitor. Same expression on both sides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Chessing234
Chessing234 force-pushed the security/screenrecord-tmp-state branch from ce438cd to 9310dd0 Compare September 12, 2026 08:06
@ErikMelton
ErikMelton merged commit b44fb74 into omacom:quattro Sep 15, 2026
mark-groves pushed a commit to mark-groves/omarchy that referenced this pull request Sep 16, 2026
…acom#8374)

* Keep screen-recording state out of world-writable /tmp

* Compare the /tmp name across the run instead of requiring it absent

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fall back to the state directory when there is no runtime dir

* Let the /tmp snapshot come back empty

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Resolve the region file the same way in the resizer

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Protect recording fallback state and document its path

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b44fb74)
mark-groves added a commit to mark-groves/omarchy that referenced this pull request Sep 16, 2026
#23)

* [Security] Keep the Windows VM password out of the RDP client's argument list

`launch_windows` passed the VM password to xfreerdp3 as `/p:"$WIN_PASS"`.
`/proc/<pid>/cmdline` is world-readable, so every other user on the machine
could read the password for as long as the session stayed open — `ps` was
enough.

The arguments now go in over stdin with `/args-from:stdin`, leaving only that
flag in the process list. The argument list itself is unchanged, and building
it as an array also fixes the unquoted `$RDP_SCALE` expansion.

(cherry picked from commit fa3f607)

* Switch TCP congestion control to BBR with fq pacing

Cubic keeps pushing until packets drop, which stands queues up in the path
on fast links. BBR paces to its estimate of bottleneck bandwidth and minimum
RTT instead, cutting queueing latency while keeping throughput. fq is the
qdisc BBR is built to pace through.

tcp_bbr and sch_fq are modules in every kernel Omarchy ships and autoload
when the sysctls are set. The migration re-applies the shipped file so new
connections switch without a reboot, no-ops once the live values match, and
flags a reboot if applying fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 9246647)

* Merge pull request omacom#7807 from Adolanium/keyring-fail-loud

Stop update-keyring from claiming success when key operations fail

(cherry picked from commit 4941894)

* [Security] Keep screen-recording state out of world-writable /tmp (omacom#8374)

* Keep screen-recording state out of world-writable /tmp

* Compare the /tmp name across the run instead of requiring it absent

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fall back to the state directory when there is no runtime dir

* Let the /tmp snapshot come back empty

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Resolve the region file the same way in the resizer

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Protect recording fallback state and document its path

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b44fb74)

* Remove unsafe project bin PATH injection (omacom#11336)

* Remove unsafe project bin PATH injection

* Cover customized unsafe Mise paths

* Revoke legacy Mise Work trust

* Harden legacy Mise trust cleanup

* Preserve ignored Mise Work configs

* Scope Mise path cleanup to env

* Accept paranoid Mise ignore marker

Reported-by: infosec-us-team
(cherry picked from commit a73bcbf)

* Treat matching kernel headers as a base system guarantee

DKMS hardware installers now assume headers are already present. Existing installs get linux-omarchy or linux-t2 headers via migration 1789444024 when those kernels are installed. The ISO package list still keeps the fork's linux/linux-ptl set until the default-kernel PR lands.

Co-authored-by: Mark Groves <mark-groves@users.noreply.github.com>

* Set up the Claude browser extension when choosing Claude

Picking Claude as the default agent now best-effort installs the official Chromium-family extension. A cancelled or failed install still selects and launches Claude. Cursor remains the official-tarball path and is not installed through mise.

Co-authored-by: Mark Groves <mark-groves@users.noreply.github.com>

---------

Co-authored-by: Paolo Pellicori <pellicori.paolo@gmail.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
Co-authored-by: Taksh Kothari <takshkothari09@gmail.com>
Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Erik Melton <meltonaerik@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mark Groves <mark-groves@users.noreply.github.com>
omarchybot added a commit to ansonboby/omarchy that referenced this pull request Sep 30, 2026
A fixed name in /tmp is one any other local account can create first, and a lock file this user cannot open makes flock fail, so every stop returns quietly and the recording cannot be stopped. The per-user runtime directory is where omacom#8374 moved the rest of the recorder's state for the same reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
omarchybot added a commit to ottosilva/omarchy that referenced this pull request Oct 1, 2026
Both this branch and omacom#8374 appended tests to the end of screenrecording-test.sh. The resolution keeps both blocks, quattro's first and this branch's webcam negotiation tests after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
omarchybot added a commit to calledtoconstruct/omarchy that referenced this pull request Oct 2, 2026
quattro moved the recorder's state out of /tmp into a private runtime dir (omacom#8374). The gsr socket follows it there, and the bar indicator and menu guard resolve the same fallback, so all three name one socket and none of them falls back to a fixed /tmp name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Screen recording keeps the in-flight recording path at a fixed /tmp name, and that path drives ffmpeg, mv and rm

5 participants