Skip to content

refactor: extract confined_join#7712

Open
erickguan wants to merge 1 commit into
mainfrom
fix/confine-compfs-monoiofs-root
Open

refactor: extract confined_join#7712
erickguan wants to merge 1 commit into
mainfrom
fix/confine-compfs-monoiofs-root

Conversation

@erickguan

Copy link
Copy Markdown
Member

Which issue does this PR close?

Complement to #7702. Merge after #7702

Rationale for this change

Better code cohesion and documentation.

What changes are included in this PR?

  • extraction of confined_join

Are there any user-facing changes?

None

AI Usage Statement

No

@erickguan erickguan requested a review from Xuanwo as a code owner June 9, 2026 02:57
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. releases-note/refactor The PR does a refactor on code or has a title that begins with "refactor" labels Jun 9, 2026

@tonghuaroot tonghuaroot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confined_join rejects .. traversal but does not guard against absolute paths. On Unix, PathBuf::join with an absolute argument replaces the base entirely:

let base = Path::new("/data/root");
assert_eq!(base.join("/etc/passwd"), Path::new("/etc/passwd")); // escapes root

I know normalize_path strips the leading / before keys reach the backend, but since confined_join is now a public API in core, it would be safer to also reject Component::RootDir (and Component::Prefix on Windows) so the function is self-contained and doesn't rely on callers to sanitize input.

@erickguan erickguan force-pushed the fix/confine-compfs-monoiofs-root branch from 3b7a7f4 to 5230cd2 Compare June 10, 2026 14:39
@erickguan

Copy link
Copy Markdown
Member Author

@tonghuaroot Good point! Anything in core/src/raw are private APIs subject to our changes. We apply normalize_path in Operator before a service sees the path. But I can see confined_join rather confusing for service maintainers.

From OpenDAL users perspective, trying something with "/etc/passwd" should never work when root is /root:

let op = Operator::new("fs", "/root")?.finish(); # pesudo code
op.read("/etc/passwd").await?; # We don't have access to this file.

normalize_path strips the leading / before keys reach the backend

normalize_path has a corner case which will return "/" when path is empty. This will also break the confined_join as you pointed out PathBuf::join will replace path with an absolute path. So a safer choice would be to reject absolute path except "/".

    let trimmed = path.trim_end_matches('/');
    if trimmed == "" {
        return Ok(base)
    }
    if Path::new(trimmed)
        .components()
        .any(|c| matches!(c, Component::ParentDir))
    {
        return Err(Error::new(
            ErrorKind::NotFound,
            "path escapes the configured root via `..`",
        )
        .with_context("path", path));
    }
    Ok(base.join(normalized))
}

Another problem is that we are losing information from this API. e.g. when we have a path dir/, our function strips the information. I believe some services rely on the trailing / so this is still not of satisfactory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

releases-note/refactor The PR does a refactor on code or has a title that begins with "refactor" size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants