Skip to content

[Security] Keep the update transcript out of world-writable /tmp - #8429

Closed
itz4blitz wants to merge 4 commits into
omacom:quattrofrom
itz4blitz:update-transcript-out-of-tmp
Closed

itz4blitz wants to merge 4 commits into
omacom:quattrofrom
itz4blitz:update-transcript-out-of-tmp

Conversation

@itz4blitz

Copy link
Copy Markdown
Contributor

Summary

omarchy-update logs the whole update session through script(1) to the fixed name /tmp/omarchy-update.log, and omarchy-update-analyze-logs reads it back after the update.

script(1) opens its transcript without O_NOFOLLOW — verified against util-linux on this machine: a symlink planted at the path is followed, and the target is truncated and written as the user running the update. With /tmp world-writable, any other local account can pre-plant that symlink and have the update session written through to a file of their choosing. Independently, the default umask leaves the transcript world-readable in /tmp, exposing the session to every account on the machine.

Same class as #8367 and #8372 (predictable /tmp staging that feeds a follow-on consumer), in files neither of those fixes covers.

Change

  • The transcript lives at ${XDG_RUNTIME_DIR}/omarchy-update.log (per-user runtime dir, 0700), falling back to ${XDG_STATE_HOME:-~/.local/state}/omarchy/omarchy-update.log when there is no session runtime dir — the same resolution [Security] Stage diagnostics where only their owner can read them #8370 uses for the diagnostics log.
  • The file is pre-created 0600 (install -m 600), so script(1) truncates an already-private file instead of creating one at the umask.
  • omarchy-update-analyze-logs resolves the identical path in both branches (it runs in the same environment, so the resolution agrees), and a standalone run with no transcript behind it is now a quiet no-op instead of grep errors.
  • docs/update-process.md updated where the path was named.

Scope

Local multi-user attack surface (symlink clobber as the updating user + world-readable transcript). Single-user machines are unaffected by the clobber; the readability half applies anywhere /tmp is shared. No network or same-user primitive introduced or changed.

Test

test/shell.d/update-transcript-staging-test.sh — stubs script so the wrapper runs without an update, and covers: the runtime-dir path, the file existing at 0600 before script opens it, the no-runtime-dir fallback matching what analyze-logs reads, analyze-logs flagging (and clearing) the initramfs failure in both branches, the standalone no-op, and that neither script names the /tmp path anymore.

Mutation-checked: restoring the /tmp path in bin/omarchy-update fails the suite; so does restoring it in omarchy-update-analyze-logs. battlenet-test, bin-style-test, update-sequence-test, update-lock-test, update-status-test, and installed-service-test still pass.

script(1) opens its transcript without O_NOFOLLOW, so the fixed name
/tmp/omarchy-update.log let any other local account pre-plant a symlink
there and have the whole update session written through to a file of
their choosing, as the user running the update. The default umask also
left the log world-readable, exposing the session transcript to every
account on the machine.

Stage it under the per-user runtime directory (0700), pre-created 0600,
falling back to the state directory where Omarchy keeps everything else
of its own when there is no session runtime dir. omarchy-update-analyze-logs
resolves the same file, and a standalone run with no transcript behind it
is now a quiet no-op rather than a grep error.

Same class as the diagnostics and screen-recording staging fixes (omacom#8367,
omacom#8372), in files neither covers.

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 update transcripts from world-writable /tmp into private per-user storage.

Changes:

  • Resolves runtime/state transcript paths consistently.
  • Pre-creates transcripts with mode 0600.
  • Adds regression tests and updates documentation.

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 2 out of 4 changed files in this pull request and generated 2 comments.

File Description
bin/omarchy-update Securely stages the transcript.
bin/omarchy-update-analyze-logs Reads the relocated transcript.
test/shell.d/update-transcript-staging-test.sh Tests paths, permissions, and analysis.
docs/update-process.md Documents transcript storage.

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

Comment thread docs/update-process.md Outdated
Comment on lines +148 to +151
- A failure should leave enough output in the update transcript
(`$XDG_RUNTIME_DIR/omarchy-update.log`, or
`${XDG_STATE_HOME:-~/.local/state}/omarchy/omarchy-update.log` without a
session) and the terminal transcript to debug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The list item is now one line and names both paths.

Comment thread docs/update-process.md
```text
omarchy-update
├─ ensure transcript logging through script(1) → /tmp/omarchy-update.log
├─ ensure transcript logging through script(1) → $XDG_RUNTIME_DIR/omarchy-update.log

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The Path 1 flow now shows the state-directory fallback when XDG_RUNTIME_DIR is unset, matching the scripts.

The high-level flow only named $XDG_RUNTIME_DIR, and the failure-output list item was hard-wrapped. Both now match the runtime/state resolution the scripts already use.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Chessing234

Copy link
Copy Markdown
Contributor

hey, this is already covered in #12109 (update transcript out of shared /tmp). mind closing this as a duplicate?

omarchybot and others added 2 commits September 28, 2026 20:51
quattro moved the transcript re-exec below the sudo cleanup setup and passes OMARCHY_UPDATE_USER_PATH through it, and added the stay-awake row to the update docs. Keep both alongside the private transcript path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
omarchy-update now starts under a privileged Bash, checks OMARCHY_PATH against its own location, and re-execs through script(1) from a fixed PATH, so the stub on the caller's PATH was never the one it ran. Invoke the boundary copy the other update tests use and put the stub in its bin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed and run on a disposable Omarchy worker. The problem is real and this fixes it; it now merges cleanly against quattro.

What the bug does on a stock install. On the worker, a second local account that plants /tmp/omarchy-update.log first, as a regular file or a symlink, makes script(1) fail with cannot open /tmp/omarchy-update.log: Permission denied, so omarchy update aborts before it starts. With nothing planted, the transcript is written 0644 and readable by every account. One correction to the description: writing through a planted symlink into another file does not happen on a stock install, because fs.protected_symlinks=1 refuses to follow it. The real effects are that another account can block updates and read the transcript. Under this PR the transcript is created 0600 under /run/user/$UID, which the worker confirmed another account cannot write to.

Pushed to your branch

  • b90dc8d merges quattro in. quattro now passes OMARCHY_UPDATE_USER_PATH through the script(1) re-exec and adds a stay-awake row to docs/update-process.md; both are kept alongside your change.
  • 7ac940f makes update-transcript-staging-test.sh run omarchy-update through test/shell.d/fixtures/sudo-boundary-test.sh, as the other update tests do. omarchy-update now starts under bash -p, checks OMARCHY_PATH, and resets PATH before the re-exec, so a script stub on the caller's PATH was never the one it ran.

Tests, on the worker: update-transcript-staging-test.sh fails on quattro (the transcript goes to /tmp/omarchy-update.log) and passes all 11 checks on this head. The 21 update-*, bin-style, security-source-root and channel-sudo-boundary shell tests pass. ./test/cli has one failure, vscode generated theme references current theme file, and it fails the same way on quattro.

Related: this also fixes #11204 (analyze-logs printing a raw grep error when there is no transcript). #12957 and #12109 fix the same bug in other ways: #12957 falls back to /tmp when there is no runtime directory and also refuses to run as root, and #12109 bundles this fix into a much larger change. Which one lands is for the maintainer.

Second opinion: none. Codex Medium's daily review budget was spent before this item reached it, so only Claude Opus 5.5 checked this, including the choice between the three pull requests. Because of that it has not been marked verified, and its classification at the new head is unsettled.

Waiting on: a review by the second model, then the maintainer. Nothing is needed from you.

@dhh

dhh commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Closing after maintainer review. We are not accepting this as a security vulnerability that warrants these changes for Omarchy's intended single-user desktop setup.

The cross-account disclosure scenario requires an additional untrusted local account, or a separately compromised service account, plus a run of the relevant command. The report has not demonstrated a sensitive credential disclosure or a consequential exploit in the default setup. Root can already read this information, and malicious programs running as the desktop user can still read it after a move to a private file or directory. Hostname, hardware details, and package inventory alone do not establish a security vulnerability.

The default systemd protections (fs.protected_symlinks and fs.protected_regular) block the described cross-user symlink redirect and writes into another user's pre-created regular file in sticky /tmp. World-writable /tmp by itself is not evidence of arbitrary file overwrite or privilege escalation. These protections do not prevent reading an existing 0644 file; that narrow cross-account exposure is acknowledged.

The default /tmp is tmpfs-backed and is cleared on reboot. These logs are not permanently retained: the exposure, where an untrusted second account exists, lasts only while the files remain during that boot. Reboot limits the exposure; it does not make cross-account reads impossible before then.

Private staging and explicit cleanup can be reasonable housekeeping, but we do not consider the demonstrated behavior a security priority for the default Omarchy threat model. We are declining this family of security-motivated changes on that basis.

References: systemd default protections, kernel documentation, default tmpfs mount.

The absence of O_NOFOLLOW in script(1) does not bypass the kernel protected_symlinks policy. A same-user symlink demonstration does not establish the claimed cross-user overwrite under default settings. The missing-log behavior in #11204 remains a separate functional issue.

Omabot on behalf of DHH

@dhh dhh closed this Oct 4, 2026
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.

5 participants