fix(data): stop the permission repair following persisted symlinks - #165
Merged
Conversation
repair_data_permissions walks the data directory and chmods every entry to 0700 or 0600. chmod dereferences, and os.walk yields a link-to-file under files and a link-to-directory under dirs, so any symlink stored under the data directory had its target re-permissioned instead, anywhere on the filesystem it happened to point. The Codex CLI puts argv[0] dispatch links under CODEX_HOME/tmp/arg0 pointing back at its own binary inside the image, and Nojoin sets CODEX_HOME to a per-user directory on the persistent data volume because the subscription auth.json has to live there. So the links persist across restarts, and a privileged pass set the bundled codex binary to 0600. worker-io then failed every Codex-backed generation with EACCES, which is issue #164. Reproduced in the shipped worker-io image and confirmed fixed there: a root pass now leaves the binary at 0755 and it still runs as uid 1000. Under the gosu-dropped runtime user the chmod always failed with EPERM and only logged, which is why the defect stayed latent rather than being caught in normal operation. Skipping is the only correction available. Linux has no lchmod, so os.chmod(..., follow_symlinks=False) raises NotImplementedError, and the directory list has to be pruned in place so the walk neither descends into a symlinked directory nor chmods it on the way past. Coreutils already behaves this way, so the chmod -R the deployment guide gives operators was never affected. Not paired with a cleanup of the Codex scratch directory, which was the other candidate fix. Codex reuses one lock-guarded arg0 directory rather than accumulating them, verified across repeated runs, and worker-io runs prefork with concurrency 4, so deleting that directory would race a live Codex process for no gain. TMPDIR does not relocate it either. Refs: #164, docs/DEPLOYMENT.md
2 tasks
Copilot stopped work on behalf of
Valtora due to an error
July 29, 2026 11:14
Valtora
added a commit
that referenced
this pull request
Jul 30, 2026
Bump docs/VERSION to 2.2.0 so the tag validates, and fill in the release-notes template for this range instead of hand-editing the published body afterwards, which is how v2.1.0 was done. A minor bump rather than a patch: #168 adds a capture action that did not exist before, stopping and processing a paused recording. Refs: #162, #163, #165, #167, #168, #169, #170, #171
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
repair_data_permissionswalks the data directory and chmods every entry to 0700 or 0600.chmoddereferences, andos.walkyields a link-to-file underfilesand a link-to-directory underdirs, so any symlink stored under the data directory had its target re-permissioned instead, anywhere on the filesystem it pointed.The Codex CLI keeps argv[0] dispatch links under
CODEX_HOME/tmp/arg0pointing back at its own binary inside the image, and Nojoin setsCODEX_HOMEto a per-user directory on the persistent data volume because the subscriptionauth.jsonhas to live there. Those links survive restarts, so a privileged pass set the bundledcodexbinary to 0600 andworker-iothen failed every Codex-backed generation withEACCES.The pass now skips symlinks: the directory list is pruned in place so the walk neither descends into a symlinked directory nor chmods it on the way past, and symlinked files are skipped before the chmod. Skipping is the only correction available, because Linux has no
lchmodandos.chmod(..., follow_symlinks=False)raisesNotImplementedError. Coreutils already behaves this way, so thechmod -Rthe deployment guide gives operators was never affected.Under the gosu-dropped runtime user the chmod always failed with
EPERMand only logged a warning, which is why the defect stayed latent in normal operation rather than being caught earlier. It becomes live the moment the pass runs privileged, which a rootdocker exec ... python ...does, since the worker image intentionally keepsUSER rootandbackend.utils.config_managerbuilds its singleton at import.Deliberately not paired with a cleanup of the Codex scratch directory, which was the other candidate fix. Codex reuses one lock-guarded
arg0directory rather than accumulating them, verified across repeated runs, andworker-ioruns prefork with concurrency 4, so deleting that directory would race a live Codex process for no gain.TMPDIRdoes not relocate it either.Existing deployments self-heal on upgrade: the 0600 lives in the container's writable overlay layer, so recreating
worker-iorestores 0755, and with this change it cannot be set again.No new dependencies.
Fixes #164
Type of change
Checks run
source .venv/bin/activate && pytestpython scripts/check.py(Ruff lint, format check, mypy, doc and Alembic validators)cd frontend && npm run lintcd frontend && npm run testcd frontend && npm run buildpython3 scripts/validate_docs.pypython3 scripts/validate_alembic.pyMigration impact
Documentation impact
docs/DEPLOYMENT.mdnow states that the repair pass skips symbolic links.Security impact
No auth, token, encryption or exposure boundary moves. The change does narrow a hardening gap as a side effect: while it stood, any symlink placed under the data directory gave the pass a chmod primitive over its target whenever the pass ran privileged.
docs/SECURITY.mddescribes no behaviour that changes here, so it is untouched.Manual verification
Reproduced and fixed inside the shipped
nojoin-worker-io:localimage, using the real Codex symlink target from the issue report and the real entrypoint sequence (root ownership repair, gosu drop to uid 1000).Before, a root pass left the binary at
uid=0 gid=0 mode=600andcodexfailed to exec as uid 1000 with permission denied. After, the same root pass leaves it atuid=0 gid=0 mode=755andcodex-cli 0.145.0runs as uid 1000.Also confirmed against the real CLI rather than a synthetic fixture: Codex writes four links (
apply_patch,applypatch,codex-execve-wrapper,codex-linux-sandbox), all pointing at the vendored binary named in the issue, and three consecutive runs produce onearg0directory rather than three.No capture, recording context-menu, or frontend surface touched.