{"record":{"id":"f4ed7b062be3a834","repo":"jdx/mise","slug":"expected-symlinked-appdir-tail-to-be-rejected","errorCode":null,"errorMessage":"expected symlinked appdir tail to be rejected","messagePattern":"expected symlinked appdir tail to be rejected","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/tests.rs","lineNumber":6681,"sourceCode":"    ensure_trusted_appdir(&appdir)?;\n    assert!(appdir.symlink_metadata()?.file_type().is_dir());\n    // Idempotent when the directory already exists.\n    ensure_trusted_appdir(&appdir)?;\n    Ok(())\n}\n\n#[test]\nfn ensure_trusted_appdir_rejects_symlinked_tail() -> Result<()> {\n    // Simulate a symlink planted on the not-yet-existing appdir tail\n    // between validation and mutation: it must be rejected, not followed.\n    let tmp = trusted_tempdir()?;\n    let base = tmp.path().canonicalize()?;\n    let elsewhere = base.join(\"elsewhere\");\n    file::create_dir_all(&elsewhere)?;\n    let appdir = base.join(\"Applications\");\n    std::os::unix::fs::symlink(&elsewhere, &appdir)?;\n    let err = match ensure_trusted_appdir(&appdir) {\n        Ok(_) => panic!(\"expected symlinked appdir tail to be rejected\"),\n        Err(err) => err.to_string(),\n    };\n    // Must fail because the tail is a symlink, not because an ancestor was\n    // untrusted (which is a different guard).\n    assert!(err.contains(\"cannot open operation directory\"), \"{err}\");\n    assert!(!err.contains(\"untrusted directory\"), \"{err}\");\n    Ok(())\n}\n\n#[test]\nfn ensure_trusted_appdir_stays_bound_after_same_uid_replacement() -> Result<()> {\n    // The reviewer's scenario: after validation, a same-uid process swaps\n    // the accepted appdir for a different directory (or symlink). Because\n    // the descriptor is retained and mutations are addressed through it,\n    // writes still land in the originally validated directory.\n    let tmp = trusted_tempdir()?;\n    let base = tmp.path().canonicalize()?;\n    let appdir = base.join(\"Applications\");","sourceCodeStart":6663,"sourceCodeEnd":6699,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/packages/brew/cask/tests.rs#L6663-L6699","documentation":"Test panic in the cask security tests: ensure_trusted_appdir was given an Applications path whose final component is a symlink to another directory, and it returned Ok. The test requires rejection with a 'cannot open operation directory' error — the tail symlink must be refused via openat-style no-follow semantics — and explicitly must NOT be rejected via the different 'untrusted directory' ancestor guard.","triggerScenarios":"Calling ensure_trusted_appdir(appdir) where appdir itself is a symlink to a directory the caller controls; the implementation follows the symlink instead of failing with a no-follow open, so the guard is bypassed.","commonSituations":"An attacker replaces ~/Applications with a symlink to a world-controlled directory so cask staging writes land outside the trusted location; regression in openat(O_NOFOLLOW|O_DIRECTORY) usage on the tail component.","solutions":["Run the test and inspect why ensure_trusted_appdir succeeded on a symlinked tail","Open the final path component with openat using O_NOFOLLOW|O_DIRECTORY on the parent fd so symlinked tails fail with 'cannot open operation directory'","Verify the error is the tail/no-follow failure, not the 'untrusted directory' ancestor failure (the test asserts both the presence and absence of messages)","Add regression coverage for symlinked intermediate components as well as the tail"],"exampleFix":"// before\nlet dir = File::open(appdir)?; // follows symlink\n// after\nlet parent = File::open(appdir.parent().unwrap())?;\nlet dir = openat(&parent, appdir.file_name().unwrap(),\n                 OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_DIRECTORY))?;\n// symlink tail => ELOOP => \"cannot open operation directory\" error","handlingStrategy":"validation","validationCode":"// reject a symlinked tail before calling ensure_trusted_appdir\nlet md = std::fs::symlink_metadata(&appdir)?;\nif md.file_type().is_symlink() {\n    panic!(\"appdir tail must not be a symlink\");\n}","typeGuard":"fn tail_is_real_dir(p: &Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| m.is_dir()).unwrap_or(false)\n    // false when the final component is a symlink\n}","tryCatchPattern":"match ensure_trusted_appdir(&appdir) {\n    Ok(()) => { /* proceed */ }\n    Err(e) if e.to_string().contains(\"cannot open operation directory\") => {\n        eprintln!(\"appdir tail is a symlink or unopenable: {e}\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Use openat with O_NOFOLLOW|O_DIRECTORY on the final component instead of path-based open","Distinguish tail-symlink failures from ancestor-trust failures in error messages so tests and users can tell guards apart","Test both symlinked tails and symlinked ancestors separately"],"tags":["rust","security","symlink","path-traversal","test-assertion"],"backgroundTag":"path-traversal-blocked","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}