rustfs/rustfs · error · io::Error

rename destination contains an invalid path component

Error message

rename destination contains an invalid path component

What it means

After the base-prefix check succeeds, mkdir_all_below_existing_base_std walks the remaining components and accepts only Normal and CurDir ('.') components. A ParentDir ('..') or RootDir component in the relative remainder is rejected with ErrorKind::InvalidInput, blocking destinations that would climb out of the base directory during recursive directory creation. It complements the strip_prefix check by validating the shape of the relative path, not just its prefix.

Source

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

#[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 9e6e02ea09)

Solutions

  1. Sanitize at the API boundary: reject '..' segments (and leading '/') in bucket and object names before they reach the disk layer
  2. Fix the specific caller emitting the path; this error marks an upstream validation gap
  3. Keep the S3-layer key-validation tests as the regression guard for this class

Example fix

// before
let object = "a/../../escape"; // reaches disk layer -> invalid path component

// after
fn is_safe_key(k: &str) -> bool {
    !k.split('/').any(|seg| seg == ".." || seg.is_empty() && false) && !k.starts_with('/')
}
assert!(is_safe_key(object), "object key rejected");
Defensive patterns

Strategy: validation

Validate before calling

fn has_parent_dir_component(p: &Path) -> bool {
    p.components().any(|c| matches!(c, std::path::Component::ParentDir))
}
if has_parent_dir_component(dst_parent) { return Err(/* invalid key */); }

Type guard

fn is_safe_object_key(key: &str) -> bool {
    !key.starts_with('/') && key.split('/').all(|seg| seg != ".." && !seg.is_empty() || seg.is_empty())
}
// enforce at the S3 API boundary

Try / catch

Match ErrorKind::InvalidInput with the 'invalid path component' message; reject the request as a malformed key and audit how it passed upstream validation.

Prevention

When it happens

Trigger: A rename destination parent containing '..' (e.g. base/bucket/../../escape) or an embedded root component, so a component other than Normal/CurDir appears after prefix stripping.

Common situations: Object keys or bucket names with '..' segments that were not sanitized upstream, migration tools copying trees that contain '..' entries, or hand-built test paths.

Related errors


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