You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cloud credential transfer tests fail on macOS: secure_directory O_NOFOLLOW ancestor walk rejects symlinked TMPDIR (/var -> /private/var) #1947
The new cloud credential transfer validation (secure_directory in crates/jcode-base/src/auth/transfer.rs) walks every HOME/TMPDIR ancestor from / with O_NOFOLLOW. On macOS the default TMPDIR is /var/folders/... and /var is a symlink to /private/var, so the ancestor walk fails with UnsafePath and 17 of the 22 auth::transfer tests fail locally on any standard macOS machine. CI macos passes only because its HOME/TMPDIR are real dirs.
Steps to reproduce
On macOS (default TMPDIR=/var/folders/...), checkout master at 7d342c1.
cargo test -p jcode-base auth::transfer
17/22 tests fail with Err(UnsafePath) at the import_at/export_at ancestor validation.
Same command with TMPDIR set to a symlink-free real dir (e.g. under /Users/alejandro.blanco): all 22 pass.
Also reproduced with TMPDIR=/tmp (symlink to /private/tmp): same 17 failures.
The related cloud acceptance path prints: cloud protocol probe failed: The credential store path is not a safe regular file or private directory. and the cli::cloud_move::latency_tests fail the same way in a scrubbed env.
Expected
Tests and the transfer feature work with the OS-default temporary directory on macOS, or the validation canonicalizes the path before the ancestor walk.
Actual
secure_directory rejects any path whose ancestors include a symlink (standard macOS layout), returning UnsafePath.
Proposed fix
Resolve symlinks once before the descriptor walk (e.g. std::fs::canonicalize on the base path, then walk the resolved path with the existing O_NOFOLLOW per-component safety), keeping the per-component checks for concurrent-rename safety. Tests should then pass with the macOS default TMPDIR.
Thanks for the clear macOS reproduction. This is security-sensitive credential-path validation, so I will not replace the no-follow walk with canonicalization without reviewing the trust boundary and symlink/rename race cases. Recommended next step: define which OS-managed ancestor aliases are permitted, retain descriptor-relative validation of the private store, and verify both default macOS TMPDIR and hostile-symlink cases. Leaving this open for that review.
Jcode agent (automated triage), on behalf of @1jehuang
thread 'auth::transfer::tests::cloud_handoff_replaces_stale_rotated_copy_but_not_other_identity' panicked at
crates/jcode-base/src/auth/transfer.rs:1277:73:
called `Result::unwrap()` on an `Err` value: UnsafePath
Cause is where the report says it is: secure_directory (crates/jcode-base/src/auth/transfer.rs:479) walks from / to the target with O_NOFOLLOW | O_DIRECTORY, and rejects the first ancestor that is not a real directory. On macOS /var is a symlink to /private/var, so every descendant of the default TMPDIR is rejected before the target is ever opened. With TMPDIR=/tmp the same thing happens (/tmp -> /private/tmp), which confirms it is the ancestor prefix, not the leaf.
So this is not a flaky test: it is the security check rejecting a path shape that every standard macOS user has. CI macOS stays green only because the runner's HOME/TMPDIR have no symlinked ancestors, which means the suite is effectively not exercising the check on the platform users run.
Left triage: needs-decision because fixing it is a policy choice about what the walk should permit, not a mechanical patch. Three options, roughly:
Canonicalize each ancestor before comparing, and keep O_NOFOLLOW on the final open only. Preserves the "do not follow a symlink into the target" property while accepting vendor path shapes.
Walk only real directories under HOME and require HOME itself to be canonical and owned by the current user, treating the TMPDIR prefix as trusted above that point.
Keep the strict walk and change the tests to build their own symlink-free scratch root. Cheapest, but leaves jcode refusing a standard macOS TMPDIR at runtime, so it hides the user-visible behavior rather than fixing it.
Option 1 is the one that matches the intent documented in the code; option 3 would make the tests pass while shipping the same runtime rejection. Happy to implement whichever is chosen.
— Jcode agent (automated triage), on behalf of @1jehuang
Activity
1jehuang commentedon Oct 24, 2026
Thanks for the clear macOS reproduction. This is security-sensitive credential-path validation, so I will not replace the no-follow walk with canonicalization without reviewing the trust boundary and symlink/rename race cases. Recommended next step: define which OS-managed ancestor aliases are permitted, retain descriptor-relative validation of the private store, and verify both default macOS TMPDIR and hostile-symlink cases. Leaving this open for that review.
Jcode agent (automated triage), on behalf of @1jehuang
ianalitis commentedon Oct 25, 2026
Reproduced on current master (
4bb16b96c), macOS 26 arm64 (Darwin 25.6.0).Same counts as reported, from the same binary, changing only
TMPDIR:Failure shape is uniform:
Cause is where the report says it is:
secure_directory(crates/jcode-base/src/auth/transfer.rs:479) walks from/to the target withO_NOFOLLOW | O_DIRECTORY, and rejects the first ancestor that is not a real directory. On macOS/varis a symlink to/private/var, so every descendant of the defaultTMPDIRis rejected before the target is ever opened. WithTMPDIR=/tmpthe same thing happens (/tmp->/private/tmp), which confirms it is the ancestor prefix, not the leaf.So this is not a flaky test: it is the security check rejecting a path shape that every standard macOS user has. CI macOS stays green only because the runner's
HOME/TMPDIRhave no symlinked ancestors, which means the suite is effectively not exercising the check on the platform users run.Left
triage: needs-decisionbecause fixing it is a policy choice about what the walk should permit, not a mechanical patch. Three options, roughly:O_NOFOLLOWon the final open only. Preserves the "do not follow a symlink into the target" property while accepting vendor path shapes.HOMEand requireHOMEitself to be canonical and owned by the current user, treating theTMPDIRprefix as trusted above that point.jcoderefusing a standard macOSTMPDIRat runtime, so it hides the user-visible behavior rather than fixing it.Option 1 is the one that matches the intent documented in the code; option 3 would make the tests pass while shipping the same runtime rejection. Happy to implement whichever is chosen.
— Jcode agent (automated triage), on behalf of @1jehuang