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

rename destination must have a file name

Error message

rename destination must have a file name

What it means

Unix guarded rename (os.rs:2339): the destination must have a final component for renameat(2), but Path::file_name() returns None when the destination terminates in ".." or is a root/empty path. The operation fails with InvalidInput before touching the filesystem, rather than letting renameat target a directory.

Source

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

    let Some(parent_guard) = parent_guard else {
        let rename_started = rustfs_io_metrics::put_stage_timer();
        let result = super::fs::rename_std(src_file_path, dst_file_path);
        rustfs_io_metrics::record_put_object_stage_duration_from(
            rustfs_io_metrics::PUT_STAGE_SET_DISK_RENAME_RENAME_SYSCALL,
            rename_started,
        );
        return result;
    };
    let src_parent = src_file_path
        .parent()
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "rename source must have a parent directory"))?;
    let src_name = src_file_path
        .file_name()
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "rename source must have a file name"))?;
    let dst_name = dst_file_path
        .file_name()
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "rename destination must have a file name"))?;
    let src_parent = open(
        src_parent,
        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
        Mode::empty(),
    )
    .map_err(io::Error::from)?;
    let dst_parent = parent_guard
        .last()
        .ok_or_else(|| io::Error::other("rename destination parent guard is empty"))?;

    let rename_started = rustfs_io_metrics::put_stage_timer();
    let result = renameat(&src_parent, src_name, dst_parent, dst_name).map_err(io::Error::from);
    rustfs_io_metrics::record_put_object_stage_duration_from(
        rustfs_io_metrics::PUT_STAGE_SET_DISK_RENAME_RENAME_SYSCALL,
        rename_started,
    );
    result
}

View on GitHub (pinned to 201c653dcd)

Solutions

  1. Log the dst path; trailing ".." or root form identifies the construction bug
  2. Validate dst.file_name().is_some() before initiating the rename
  3. Reject ".." components and empty names in object keys at the API boundary

Example fix

// before
rename_all(src, &dst_from_client, &base, &root, lease).await?;

// after
if dst_from_client.file_name().is_none() {
    return Err(io::Error::new(io::ErrorKind::InvalidInput, "rename destination must name a file"));
}
rename_all(src, &dst_from_client, &base, &root, lease).await?;
Defensive patterns

Strategy: validation

Validate before calling

if dst.file_name().is_none() {
    return Err(io::Error::new(io::ErrorKind::InvalidInput, "rename destination must name a file"));
}

Type guard

fn has_final_file_name(p: &Path) -> bool { p.file_name().is_some() }

Try / catch

match rename_all(src, &dst, &base, &root, lease).await {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput
        && e.to_string().contains("destination must have a file name") => {
        // dst ends in ".." or is a root: fix destination construction
    }
    other => other?,
}

Prevention

When it happens

Trigger: Rename destination built from an object/bucket whose path collapses to a trailing ".." or to "/" while the guard branch is active (destination below base_dir required directory creation).

Common situations: Destination keys with "../" segments; empty destination strings; path join bugs producing root-form destinations on Unix hosts.

Related errors


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