rustfs/rustfs · error · std::io::Error

rename destination contains an invalid path component

Error message

rename destination contains an invalid path component

What it means

After stripping base_dir, the relative destination path contains a component that is not Normal or CurDir — typically ParentDir (`..`), or a root/prefix component leaking in. The guarded create only walks plain named components, so it rejects the path with InvalidInput before touching the filesystem.

Solutions

  1. Lexically clean the relative path (drop ParentDir components) before calling
  2. Validate user input at the API boundary so traversal segments never reach the disk layer
  3. Unit-test the path builder with dot-dot, dot, and empty components

Example fix

// before
let dir = base.join(format!("{}/../tmp", rel));

// after
let dir = base.join(rel).join("tmp");
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_relative(rel: &std::path::Path) -> bool {
    rel.components()
        .all(|c| matches!(c, std::path::Component::Normal(_) | std::path::Component::CurDir))
}

Try / catch

Err(err) if err.kind() == io::ErrorKind::InvalidInput && err.to_string().contains("invalid path component") => reject the input as a traversal attempt; no retry

Prevention

When it happens

Trigger: dir_path like <base>/../escape strips to ../escape; paths assembled from unnormalized user input containing dot-dot segments.

Common situations: User-supplied object or bucket names containing traversal segments after a buggy join; URL-decoded prefixes reintroducing dot-dot.

Related errors


AI-assisted analysis of rustfs/rustfs@201c653dcd (2026-08-20). Data as JSON: /api/errors/7308d47dc363cee0. Report an issue: GitHub.

Appendix: source

Thrown at crates/ecstore/src/disk/os.rs:3742

#[cfg(windows)]
fn windows_rename_source_is_allowed(attributes: u32, reparse_tag: u32) -> bool {
    use windows_sys::Win32::{Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT, System::SystemServices::IO_REPARSE_TAG_DEDUP};

    attributes & FILE_ATTRIBUTE_REPARSE_POINT == 0 || reparse_tag == IO_REPARSE_TAG_DEDUP
}

pub(crate) fn mkdir_all_below_existing_base_std(
    dir_path: &Path,
    base_dir: &Path,
    publication_root: &PublicationRoot,
) -> io::Result<ExistingBaseDirectoryGuard> {
    let relative = dir_path
        .strip_prefix(base_dir)
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename destination must remain below its base directory"))?;
    for component in relative.components() {
        if !matches!(component, Component::Normal(_) | Component::CurDir) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "rename destination contains an invalid path component",
            ));
        }
    }

    #[cfg(unix)]
    {
        let _ = publication_root;
        use rustix::fs::{Mode, OFlags, mkdirat, open, openat};
        use rustix::io::Errno;

        let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
        let mode = Mode::RWXU | Mode::RWXG | Mode::RWXO;
        let mut parents = vec![open(base_dir, flags, Mode::empty()).map_err(io::Error::from)?];

        for component in relative.components() {
            let Component::Normal(component) = component else {

View on GitHub (pinned to 201c653dcd)