unicity-aos/aos-ce · error · std::io::Error

temporary product manifest path must be a regular file

Error message

temporary product manifest path must be a regular file

What it means

During ensure_unicity_ce_manifest, the temporary file path <manifest>.toml.tmp is checked with symlink_metadata before writing; if it exists as a symlink or non-regular file the function returns InvalidInput "temporary product manifest path must be a regular file". A normal leftover regular temp file is simply removed; only dangerous non-regular entries are refused, guarding against symlink attacks on the temp path.

Solutions

  1. Inspect `ls -la <manifest>.toml.tmp` and remove the offending symlink/special file, then retry.
  2. Clear the distributions directory of stale temp files after abnormal terminations.
  3. Run the process under a dedicated user so a hostile local user cannot plant entries in the private directory.

Example fix

// before (shell, failing state)
ls -la $ROOT/distributions/product.toml.tmp  # lrwxrwxrwx -> /etc/shadow
// after
rm -f $ROOT/distributions/product.toml.tmp && retry ensure_unicity_ce_manifest
Defensive patterns

Strategy: validation

Validate before calling

fn tmp_path_is_safe(tmp: &Path) -> Result<(), String> {
    if let Ok(m) = std::fs::symlink_metadata(tmp) {
        if m.file_type().is_symlink() || !m.is_file() {
            return Err(format!("{:?} is a symlink or special file; delete it before writing", tmp));
        }
    }
    Ok(())
}

Type guard

fn tmp_is_absent_or_regular(path: &Path) -> bool {
    match std::fs::symlink_metadata(path) {
        Ok(m) => m.is_file() && !m.file_type().is_symlink(),
        Err(_) => true, // absent is fine
    }
}

Try / catch

if let Err(e) = runtime.ensure_unicity_ce_manifest() {
    if e.to_string().contains("temporary product manifest") {
        let _ = std::fs::remove_file(manifest_path.with_extension("toml.tmp"));
        // retry once
    } else { return Err(e.into()); }
}

Prevention

When it happens

Trigger: Calling ensure_unicity_ce_manifest when <manifest>.toml.tmp exists in the distributions directory as a symlink, directory, fifo, or other non-regular file.

Common situations: A previous crashed/aborted write left a temp entry that something replaced with a symlink; a malicious actor pre-planted a symlink at the predictable .tmp path; manual cleanup accidentally created a directory there.

Related errors


AI-assisted analysis of unicity-aos/aos-ce@f6f22024fb (2026-09-13). Data as JSON: /api/errors/edb9dbc68d8b8b9e. Report an issue: GitHub.

Appendix: source

Thrown at crates/unicity-aos-bootstrap/src/lib.rs:214

            Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "product manifest path must be a regular file",
                ));
            }
            Ok(_) if fs::read(&path)?.as_slice() == manifest.as_bytes() => return Ok(path),
            Ok(_) => {}
            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
            Err(error) => return Err(error),
        }
        self.ensure_layout()?;
        create_private_dir(&self.root.join("distributions"))?;
        let parent = path.parent().expect("manifest path has a parent");
        create_private_dir(parent)?;
        let temporary = path.with_extension("toml.tmp");
        if let Ok(metadata) = fs::symlink_metadata(&temporary) {
            if metadata.file_type().is_symlink() || !metadata.is_file() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "temporary product manifest path must be a regular file",
                ));
            }
            fs::remove_file(&temporary)?;
        }
        fs::write(&temporary, manifest)?;
        set_private_file_permissions(&temporary)?;
        fs::rename(&temporary, &path)?;
        Ok(path)
    }

    /// Initialize the trusted CE system fleet before the runtime performs its
    /// daemon-backed grant preflight.
    ///
    /// A completely fresh Astrid home has no capsule capable of accepting CLI
    /// connections. Astrid installs through the daemon in bounded batches.
    /// Resume partial batches after the kernel's rate-limit window, leaving

View on GitHub (pinned to f6f22024fb)