osfs: Adopt os.Root for path containment, add RootOS, refresh helpers/tests#211
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR modernizes the osfs implementation by switching path containment from filepath-securejoin to Go’s native os.Root, and introduces a new RootOS variant for caller-managed root lifecycle. It also updates tests/benchmarks and adds CI coverage by running go-git integration tests against the local go-billy checkout.
Changes:
- Replace
filepath-securejoincontainment withos.Root, addingRootOS(FromRoot) and updatingBoundOSto use per-operationos.Root. - Refresh and expand cross-filesystem tests (especially around symlinks, chroot behavior, and escape errors).
- Add a
go-gitintegration GitHub Actions workflow; removefilepath-securejoindependency fromgo.mod/go.sum.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
test/symlink_test.go |
Normalizes Readlink assertions across platforms (slash vs host separators). |
test/fs_test.go |
Updates expected error for containment escapes to osfs.ErrPathEscapesParent. |
osfs/os.go |
Updates OS FS factory docs/behavior to always return BoundOS backed by os.Root. |
osfs/os_rootfs.go |
Adds RootOS, FromRoot, and shared containment/error-translation helpers. |
osfs/os_options.go |
Moves Option type behind !js build tag for consistent builds. |
osfs/os_js.go |
Keeps js/wasm option/types available; adds Option type definition for js builds. |
osfs/os_bound.go |
Rewrites BoundOS to open/close os.Root per operation; refactors containment logic. |
osfs/os_bound_windows_test.go |
Adds Windows-specific path normalization tests (drive + slash-prefixed forms). |
osfs/os_bound_test.go |
Overhauls tests for new os.Root containment behavior and new RootOS. |
osfs/os_bench_test.go |
Expands benchmarks to compare BoundOS vs RootOS vs stdlib. |
helper/chroot/chroot.go |
Updates chroot helper docs and refines symlink target rewrite/readlink translation. |
helper/chroot/chroot_test.go |
Adds tests for readlink normalization/preservation across edge cases (incl. Windows forms). |
go.mod |
Drops filepath-securejoin dependency. |
go.sum |
Removes checksums for dropped dependency. |
embedfs/embed.go |
Tightens documentation for embed-backed read-only filesystem semantics. |
.github/workflows/go-git-integration.yml |
Adds CI job to test go-git against the PR’s local go-billy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Paulo Gomes <paulo@entire.io>
This change removes the previous on-demand costly evaluation of paths
with the Go's traversal resistent primitive os.Root.
The benchmarks in /test/ indicate that in most scenarios this represent
a positive performance:
│ base.txt │ pr.txt │
│ sec/op │ sec/op vs base │
Compare/osfs.boundOS_open-4 15.78µ ± 4% 13.38µ ± 2% -15.22% (p=0.002 n=6)
Compare/osfs.boundOS_read-4 88.45µ ± 1% 91.08µ ± 0% +2.97% (p=0.002 n=6)
Compare/osfs.boundOS_write-4 924.4µ ± 23% 867.2µ ± 18% ~ (p=0.180 n=6)
Compare/osfs.boundOS_create-4 31.39µ ± 51% 23.10µ ± 72% ~ (p=0.065 n=6)
Compare/osfs.boundOS_stat-4 9.493µ ± 2% 4.244µ ± 2% -55.30% (p=0.002 n=6)
Compare/osfs.boundOS_rename-4 68.14µ ± 1% 42.76µ ± 3% -37.26% (p=0.002 n=6)
Compare/osfs.boundOS_remove-4 30.14µ ± 2% 24.31µ ± 3% -19.34% (p=0.002 n=6)
Compare/osfs.boundOS_mkdirall-4 18.25µ ± 351% 17.74µ ± 303% ~ (p=0.699 n=6)
Compare/osfs.boundOS_tempfile-4 39.63µ ± 5% 34.35µ ± 2% -13.31% (p=0.002 n=6)
│ base.txt │ pr.txt │
│ B/op │ B/op vs base │
Compare/osfs.boundOS_open-4 1032.0 ± 0% 424.0 ± 0% -58.91% (p=0.002 n=6)
Compare/osfs.boundOS_read-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹
Compare/osfs.boundOS_write-4 1507.0 ± 3% 261.0 ± 0% -82.68% (p=0.002 n=6)
Compare/osfs.boundOS_create-4 1536.5 ± 3% 278.0 ± 1% -81.91% (p=0.002 n=6)
Compare/osfs.boundOS_stat-4 1120.0 ± 0% 240.0 ± 0% -78.57% (p=0.002 n=6)
Compare/osfs.boundOS_rename-4 4845.0 ± 0% 122.0 ± 1% -97.48% (p=0.002 n=6)
Compare/osfs.boundOS_remove-4 1055.00 ± 0% 63.00 ± 0% -94.03% (p=0.002 n=6)
Compare/osfs.boundOS_mkdirall-4 2123.5 ± 20% 199.0 ± 8% -90.63% (p=0.002 n=6)
Compare/osfs.boundOS_tempfile-4 183.0 ± 1% 336.0 ± 0% +83.61% (p=0.002 n=6)
│ base.txt │ pr.txt │
│ allocs/op │ allocs/op vs base │
Compare/osfs.boundOS_open-4 21.000 ± 0% 9.000 ± 0% -57.14% (p=0.002 n=6)
Compare/osfs.boundOS_read-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹
Compare/osfs.boundOS_write-4 25.000 ± 4% 8.000 ± 0% -68.00% (p=0.002 n=6)
Compare/osfs.boundOS_create-4 26.000 ± 4% 8.000 ± 0% -69.23% (p=0.002 n=6)
Compare/osfs.boundOS_stat-4 19.000 ± 0% 3.000 ± 0% -84.21% (p=0.002 n=6)
Compare/osfs.boundOS_rename-4 68.000 ± 0% 8.000 ± 0% -88.24% (p=0.002 n=6)
Compare/osfs.boundOS_remove-4 19.000 ± 0% 3.000 ± 0% -84.21% (p=0.002 n=6)
Compare/osfs.boundOS_mkdirall-4 32.500 ± 14% 8.000 ± 12% -75.38% (p=0.002 n=6)
Compare/osfs.boundOS_tempfile-4 6.000 ± 0% 14.000 ± 0% +133.33% (p=0.002 n=6)
A new ErrPathEscapesParent was introduced to represent when an operation is
attempting to escape the root/bound dir used for the bound OS.
Signed-off-by: Paulo Gomes <pjbgf@linux.com>
Capabilities() used bitwise AND instead of OR, reporting no capabilities. translateError() swallowed errors when Unwrap returned nil and relied on exact string matching. Stat() created the base dir outside os.Root as a side-effect. Abs-to-relative conversion was duplicated across six methods. MkdirAll() silently discarded the caller's permission mode. Chroot() downgraded FromRoot instances to per-operation mode. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Cache os.Root in newBoundOS to eliminate per-operation OpenRoot/Close syscall pairs. Defer symlink resolution in OpenFile to a retry path that only triggers on path-escape errors, removing an Lstat from every non-create open. Simplify fsRoot and Chroot now that os.Root is always cached. Add benchmarks for Create, Stat, Lstat, Rename and Remove. Fix WalkDir benchmark to use fs.WalkDir via iofs adapter for all implementations. Exclude setup cost from Rename and Remove benchmarks. Add TestOpenAbsSymlinkInsideRoot to prove the symlink fallback in OpenFile is needed: os.Root rejects absolute symlink targets that point inside the root, and BoundOS resolves them to relative paths. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Introduce RootOS, a high-performance filesystem backed by a caller-managed os.Root that is reused across all operations. BoundOS becomes a thin wrapper that opens and closes an os.Root per operation, avoiding leaked directory handles on Windows. FromRoot now returns (*RootOS, error), validating the root upfront rather than checking on every operation. RootOS.Chroot returns a child RootOS; BoundOS.Chroot returns a child BoundOS. Add rootOS sub-benchmarks alongside the existing implementations. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Chroot previously opened a new os.Root for the child path that was never closed, leaking a file descriptor on every call. Wrap the existing RootOS via helper/chroot instead, reusing the caller-managed parent os.Root for all operations. Document that containment remains anchored at the parent root rather than the chroot path; callers needing a tighter boundary should open a new os.Root and wrap it with FromRoot. Assisted-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Entire-Checkpoint: 3c52307e49ba
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Adjusts ChrootHelper.Readlink doc to match the implementation (relative targets are returned unchanged), enriches the "not a dir" and Rename error messages with the offending paths, documents RootOS.Chroot's auto-create side effect, reintroduces WithChrootOS as a deprecated no-op for API compatibility with go-git, and expands TestStatBaseFile to cover ".", "./" and "./.". Assisted-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
ChrootOS no longer exists; drop the WithChrootOS option, the ChrootOSFS type constant, and the js/wasm newChrootOS shim, along with the tests and API-compat assertions that referenced them. Assisted-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
The option was a no-op in both build tags: on non-js the value was never read, and on js it was stored on BoundOS and propagated through Chroot but never consulted by path resolution. Containment is now handled by os.Root (non-js) and the chroot helper (js), making deduplication moot. Drops the option, the deduplicatePath fields, and the variadic bool on newBoundOS, along with the related test and API-compat assertion. Assisted-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overhauls the
osfspackage, replacing thefilepath-securejoin-based path containment with Go's nativeos.Root(introduced in Go 1.24). It also reworks thechrootandmounthelpers, expands cross-implementation tests, adds a go-git integration job to CI, and tightens documentation across every filesystem implementation.Removes any references to the deduplication logic and the previous
ChrootOSFS.Benchstat
Less memory churn and less allocations: