{"record":{"id":"f13df790793b2ddd","repo":"astrid-runtime/astrid","slug":"layout-migration-receipt-is-redirected-or-not-a-re","errorCode":null,"errorMessage":"layout migration receipt is redirected or not a regular file: {}","messagePattern":"layout migration receipt is redirected or not a regular file: (.+?)","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/dirs_layout.rs","lineNumber":345,"sourceCode":"\n    pub(super) fn retire_verified_legacy_source(&self) -> io::Result<()> {\n        let receipt_path = self.migrations_dir().join(LAYOUT_MIGRATION_RECEIPT);\n        let intent_path = self.migrations_dir().join(LAYOUT_MIGRATION_INTENT);\n        match std::fs::symlink_metadata(&receipt_path) {\n            Err(error) if error.kind() == io::ErrorKind::NotFound => {\n                if path_entry_present(&self.state_db_path())?\n                    || path_entry_present(&self.cow_dir())?\n                    || path_entry_present(&intent_path)?\n                {\n                    return Err(io::Error::new(\n                        io::ErrorKind::InvalidData,\n                        \"layout-two home contains legacy state without a completion receipt\",\n                    ));\n                }\n                return Ok(());\n            },\n            Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    format!(\n                        \"layout migration receipt is redirected or not a regular file: {}\",\n                        receipt_path.display()\n                    ),\n                ));\n            },\n            Ok(_) => {},\n            Err(error) => return Err(error),\n        }\n\n        let receipt: LayoutMigrationReceiptV1 = read_canonical_record(&receipt_path)?;\n        let intent: LayoutMigrationRecordV1 = read_canonical_record(&intent_path)?;\n        let retirement_path = self.migrations_dir().join(LAYOUT_MIGRATION_RETIREMENT);\n        let retirement: Option<LayoutRetirementV1> = match read_canonical_record(&retirement_path) {\n            Ok(retirement) => Some(retirement),\n            Err(error) if error.kind() == io::ErrorKind::NotFound => None,\n            Err(error) => return Err(error),","sourceCodeStart":327,"sourceCodeEnd":363,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/dirs_layout.rs#L327-L363","documentation":"retire_verified_legacy_source checks the completion receipt at var/migrations/layout-v1-to-v2.complete with symlink_metadata and requires it to be a regular file, not a symlink and not a directory or other special entry. This is a no-follow security check: a redirected receipt path could make the library read attacker-controlled migration records or delete the wrong tree. Returned as InvalidData with the offending path in the message.","triggerScenarios":"Calling complete_layout_v2 when the receipt path is a symlink (e.g. someone linked it to /tmp or another volume), a directory, a FIFO/device node, or was replaced by any non-regular file.","commonSituations":"Malicious or careless symlinking inside var/migrations/; a restore tool that recreated the receipt as a symlink or directory; overlay/bind mounts presenting the receipt as something other than a regular file.","solutions":["Inspect var/migrations/layout-v1-to-v2.complete with ls -la: if it is a symlink or directory, remove it and restore the original regular receipt file from backup","Re-run the v1→v2 migration from a clean legacy sentinel state so a fresh, genuine receipt is written","Audit the var/migrations/ directory for tampering; ensure it is a private (0700) directory owned by the Astrid user","Never hand-create the receipt as a symlink or placeholder — it must be a canonical regular file written by the migration itself"],"exampleFix":"// before: receipt is a symlink\nls -la ~/.astrid/var/migrations/layout-v1-to-v2.complete  # -> symlink\nhome.complete_layout_v2(&target)?; // InvalidData: redirected receipt\n// after\nrm ~/.astrid/var/migrations/layout-v1-to-v2.complete  # remove redirect\n# restore genuine receipt from backup or re-run migration from legacy sentinel","handlingStrategy":"validation","validationCode":"use std::os::unix::fs::FileTypeExt;\nlet md = std::fs::symlink_metadata(&receipt_path)?;\nif md.file_type().is_symlink() || !md.is_file() {\n    return Err(anyhow!(\"receipt is redirected or not a regular file\"));\n}","typeGuard":"fn is_regular_no_follow(p: &Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| m.is_file() && !m.file_type().is_symlink()).unwrap_or(false)\n}","tryCatchPattern":"match home.complete_layout_v2(&target) {\n    Err(e) if e.to_string().contains(\"redirected or not a regular file\") => {\n        // remove the symlink/odd entry and restore the genuine receipt\n    },\n    r => r?,\n}","preventionTips":["Never place symlinks anywhere inside the Astrid home","Keep var/migrations/ private (0700) and owned by the service account","After restores, audit for symlinks: find ~/.astrid -type l","Re-run migration from legacy sentinel state instead of recreating receipts by hand"],"tags":["io","security","symlink","receipt"],"backgroundTag":"path-traversal-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}