Repository navigation
Add Os::SandboxedFile and Os::FilePathUtils for directory-sandboxed file access - #5320
Conversation
|
|
||
| // Helper: copy the raw (unresolved) absolute path into resolvedOut. | ||
| // If path is relative, it is prepended with baseDir. | ||
| static Status buildAbsolutePath(const char* path, const char* baseDir, char* resolvedOut, FwSizeType resolvedSize) { |
| // - Empty or `.` segments are skipped (no-op) | ||
| // - `..` segments back up writePos to the previous '/' | ||
| // - Normal segments are shifted left (via memmove) to writePos | ||
| static Status resolveInPlace(char* resolvedOut) { |
| return VALID; | ||
| } | ||
|
|
||
| Status resolvePath(const char* path, const char* baseDir, char* resolvedOut, FwSizeType resolvedSize) { |
| // Internal containment check on already-resolved paths. | ||
| // Verifies that resolvedPath starts with allowedDirectory as a prefix, | ||
| // and that the match occurs at a `/` boundary. | ||
| Status checkContainment(const char* resolvedPath, const char* allowedDirectory) { |
|
|
||
| namespace Os { | ||
|
|
||
| SandboxedFile::SandboxedFile() : m_file(), m_allowedDirectory("/"), m_configured(true) {} |
| } | ||
| } | ||
|
|
||
| void SandboxedFile::configure(const char* allowedDirectory) { |
| return m_configured; | ||
| } | ||
|
|
||
| Os::FileInterface::Status SandboxedFile::open(const char* path, |
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
Coverage report — base
|
| Module | Line | Δ | Function | Δ | Branch | Δ |
|---|---|---|---|---|---|---|
Svc/FprimeDeframer |
98.15 | -1.85 | 100.00 | +0.00 | 96.00 | -4.00 |
Modules changed
| Module | Line | Δ | Function | Δ | Branch | Δ |
|---|---|---|---|---|---|---|
Svc/FprimeDeframer |
98.15 | -1.85 | 100.00 | +0.00 | 96.00 | -4.00 |
Os/Posix |
62.00 | -0.40 | 84.21 | +0.00 | 43.87 | -0.23 |
Svc/FileUplink |
92.46 | +0.11 | 96.67 | +0.12 | 80.95 | +0.00 |
Os |
21.23 | +6.40 | 23.30 | +5.66 | 18.96 | +7.31 |
Modules without UTs
CFDP/Checksum/GTest, Drv/ByteStreamDriverModel, Drv/Interfaces, Drv/LinuxGpioDriver, Drv/LinuxI2cDriver, Drv/LinuxSpiDriver, Drv/LinuxUartDriver, Drv/Ports, Drv/Ports/DataTypes, FppTestProject/FppTest/interfaces, FppTestProject/FppTest/topology/async, FppTestProject/FppTest/topology/components/Comp, FppTestProject/FppTest/topology/components/Framework, FppTestProject/FppTest/topology/components/Receiver, FppTestProject/FppTest/topology/components/Sender, FppTestProject/FppTest/topology/guarded, FppTestProject/FppTest/topology/ports, FppTestProject/FppTest/topology/sync, FppTestProject/FppTest/topology/top_ports, FppTestProject/FppTest/topology/types, Fw/Cmd, Fw/Com, Fw/Comp, Fw/FilePacket/GTest, Fw/Fpy, Fw/Interfaces, Fw/Obj, Fw/Port, Fw/Ports/CompletionStatus, Fw/Ports/Ready, Fw/Ports/Signal, Fw/Ports/SuccessCondition, Fw/Prm, Fw/SerializableFile/test/TestSerializable, Fw/Sm, Fw/Test, Fw/Types/GTest, Os/Models, Svc/Cycle, Svc/DpPorts, Svc/Fatal, Svc/FatalHandler, Svc/FileDownlinkPorts, Svc/FprimeProtocol, Svc/Interfaces, Svc/PassiveConsoleTextLogger, Svc/Ping, Svc/PolyIf, Svc/Ports/CommsPorts, Svc/Ports/FilePorts, Svc/Ports/OsTimeEpoch, Svc/Ports/TlmPacketizerPorts, Svc/Ports/VersionPorts, Svc/Sched, Svc/Seq, Svc/Subtopologies/CdhCore, Svc/Subtopologies/ComCcsds, Svc/Subtopologies/ComFprime, Svc/Subtopologies/ComLoggerTee, Svc/Subtopologies/DataProducts, Svc/Subtopologies/DpCompression, Svc/Subtopologies/FileHandling, Svc/Types/TlmPacketizerTypes, Svc/WatchDog, TestDeploymentsProject/Ref/PingReceiver, TestDeploymentsProject/Ref/RecvBuffApp, TestDeploymentsProject/Ref/SendBuffApp, TestDeploymentsProject/Ref/Top, TestDeploymentsProject/Ref/TypeDemo, cmake/test/data/TestDeployment/TestBuildAutocoder, cmake/test/data/TestDeployment/TestChainedAutocoder, cmake/test/data/TestDeployment/TestHeaderAutocoder, cmake/test/data/TestDeployment/TestTargetAutocoder, cmake/test/data/test-fprime-library/TestLibrary/TestComponent, cmake/test/data/test-fprime-library2/TestLibrary2/TestComponent
|
|
||
| // Helper: copy the raw (unresolved) absolute path into resolvedOut. | ||
| // If path is relative, it is prepended with baseDir. | ||
| static Status buildAbsolutePath(const char* path, const char* baseDir, char* resolvedOut, FwSizeType resolvedSize) { |
|
|
||
| // Helper: copy the raw (unresolved) absolute path into resolvedOut. | ||
| // If path is relative, it is prepended with baseDir. | ||
| static Status buildAbsolutePath(const char* path, const char* baseDir, char* resolvedOut, FwSizeType resolvedSize) { |
| // - Empty or `.` segments are skipped (no-op) | ||
| // - `..` segments back up writePos to the previous '/' | ||
| // - Normal segments are shifted left (via memmove) to writePos | ||
| static Status resolveInPlace(char* resolvedOut) { |
| return VALID; | ||
| } | ||
|
|
||
| Status resolvePath(const char* path, const char* baseDir, char* resolvedOut, FwSizeType resolvedSize) { |
| //! | ||
| //! Note: path validation is purely textual (no `realpath()` calls) — it does not follow | ||
| //! symlinks. For deployments where symlink-based escapes are a concern, additional | ||
| //! measures are needed. |
There was a problem hiding this comment.
[Security] could fix general-vulnerability/symlink-toctou: Textual path validation does not follow symlinks — an attacker who can create symlinks inside the sandbox directory could escape containment.
The PR documents this limitation (line 26: "does not follow symlinks") and lists realpath() integration as future work. On embedded targets without symlink support this is a non-issue; on POSIX targets with untrusted local access it becomes exploitable. Consider documenting the threat model assumption (no symlink creation by untrusted actors) explicitly in the class-level doc comment.
| Status resolvePath(const Fw::ConstStringBase& path, const Fw::ConstStringBase& baseDir, Fw::StringBase& resolvedOut) { | ||
| // SAFETY: write directly into resolvedOut's internal buffer to avoid a temporary copy. | ||
| // resolvePath always null-terminates the output, keeping the StringBase in a valid state. | ||
| char* outBuffer = const_cast<char*>(resolvedOut.toChar()); |
There was a problem hiding this comment.
[C++ Design] could fix CPP-10 (const_cast without full justification): const_cast<char*>(resolvedOut.toChar()) writes directly into a StringBase's internal buffer, bypassing the class's encapsulation.
The SAFETY comment explains the motivation (avoid a temporary buffer copy), but this pattern couples the implementation to the assumption that toChar() returns a writable buffer whose lifetime and capacity match the StringBase's internal storage. If StringBase ever changes its storage model, this will silently corrupt. Consider using a local char[] buffer and then assigning to resolvedOut via format/copy.
| //! // -> NO_PERMISSION | ||
| //! ``` | ||
| //! | ||
| class SandboxedFile { |
There was a problem hiding this comment.
[C++ Design] must fix CPP-17 (rule of three/five): SandboxedFile has a user-defined destructor (line 47) that closes the file, but does not declare or = delete copy/move constructors or assignment operators.
Since Os::File is likely non-copyable, the implicit copy operations are probably ill-formed, but they should be explicitly deleted to prevent accidental misuse and to satisfy the rule-of-three/five invariant.
| class SandboxedFile { | |
| class SandboxedFile { | |
| public: | |
| SandboxedFile(const SandboxedFile&) = delete; | |
| SandboxedFile& operator=(const SandboxedFile&) = delete; |
| //! and verifying that a resolved path falls within an allowed directory prefix. | ||
| //! These utilities can be used independently of SandboxedFile for any path-validation need. | ||
| //! | ||
| namespace FilePathUtils { |
There was a problem hiding this comment.
[Documentation] must fix stale-sdd: Os/docs/sdd.md does not mention the new FilePathUtils or SandboxedFile utilities.
The Os SDD's section 2.2 "File system" table lists File, Directory, and FileSystem but does not include the two new utilities this PR adds. Since these are public APIs in the Os layer, the SDD should be updated with entries for both. A row for each in the §2.2 table (or a new §3 "Derived Services" subsection) would satisfy this.
| //! | ||
| //! \param directory: absolute path of the allowed uplink directory (must end with `/`) | ||
| //! | ||
| void configure(const char* directory); |
There was a problem hiding this comment.
[Documentation] suggestion stale-sdd: Svc/FileUplink/docs/sdd.md does not mention the new configure() method or sandbox behavior.
The PR adds a public configure(const char* directory) method that restricts file writes to a sandbox directory. The SDD's §3 "Design" section should note this opt-in security feature and its default behavior (sandbox = /, all paths allowed). A one-line addition to §3.1 "Assumptions" would suffice.
|
|
||
| namespace Os { | ||
|
|
||
| //! \brief A wrapper around Os::File that restricts file access to an allowed directory |
There was a problem hiding this comment.
[Documentation] suggestion missing-doc-for-new-surface: New Os::SandboxedFile is a reusable security mechanism but has no how-to guide or user-manual entry.
The Doxygen comments are thorough. However, since this utility is intended for adoption by multiple components (FileUplink, CFDP receiver, etc.), a brief entry in docs/how-to/ (e.g., "sandboxing file access") would help downstream users discover and adopt it.
| char resolved[FilePathUtils::MAX_PATH_LENGTH]; | ||
| const FilePathUtils::Status resolveStatus = | ||
| FilePathUtils::resolveFromCwd(allowedDirectory, resolved, sizeof(resolved)); | ||
| FW_ASSERT(resolveStatus == FilePathUtils::VALID); |
There was a problem hiding this comment.
[Design] could fix design-fault-handling-gap: configure() uses FW_ASSERT for error handling (lines 21, 22, 28, 32-33) instead of returning a status code.
The doc comment notes "Asserts on failure" and "Intended to be called once at startup," which justifies asserts for programming errors (nullptr, file-already-open). However, the assert on resolveStatus == VALID (line 28) could fail due to an environment issue (CWD unavailable) rather than a programming error. Returning a status would let the caller handle this gracefully.
| ASSERT_EQ(Os::FilePathUtils::VALID, status); | ||
| ASSERT_STREQ("/data/uplink/file.bin", resolved.toChar()); | ||
| } | ||
|
|
There was a problem hiding this comment.
[Test Quality] must fix test-coverage-missing-new-fpp: checkContainment() is a public API in Os::FilePathUtils but has no dedicated unit tests.
The function is tested indirectly through SandboxedFileTest, but as a public API it should have its own test cases covering: exact prefix match, prefix sharing a common start but diverging at a non-/ boundary (e.g., /data/upload vs /data/uploadevil/), root sandbox (/), empty strings, and the special case where path equals allowed directory minus trailing /.
| auto status = Os::FilePathUtils::resolvePath(path, baseDir, resolved); | ||
| ASSERT_EQ(Os::FilePathUtils::VALID, status); | ||
| ASSERT_STREQ("/data/uplink/file.bin", resolved.toChar()); | ||
| } |
There was a problem hiding this comment.
[Test Quality] suggestion test-coverage-missing-new-fpp: resolveFromCwd() is a public API but has no dedicated unit tests.
It's tested indirectly through SandboxedFile, but a direct test would verify behavior for both relative and absolute inputs without the sandbox layer.
| } | |
| TEST_F(FilePathUtilsResolveTest, ResolveFromCwdAbsolute) { | |
| char resolved[Os::FilePathUtils::MAX_PATH_LENGTH]; | |
| auto status = Os::FilePathUtils::resolveFromCwd("/data/file.bin", resolved, sizeof(resolved)); | |
| ASSERT_EQ(Os::FilePathUtils::VALID, status); | |
| ASSERT_STREQ("/data/file.bin", resolved); | |
| } |
| TEST_F(SandboxedFileTest, GetSandboxDirectoryDefault) { | ||
| Os::SandboxedFile file; | ||
| ASSERT_STREQ("/", file.getSandboxDirectory()); | ||
| } |
There was a problem hiding this comment.
[Test Quality] suggestion test-fail-path-missing: No test for SandboxedFile::open() with an empty path or a path exceeding MAX_PATH_LENGTH.
These failure paths exercise the error-handling code in resolveFromCwd and resolvePath. A test confirming that open() returns NO_PERMISSION (not an assert crash) on malformed input would strengthen the safety contract.
lestarch-autobot
left a comment
There was a problem hiding this comment.
Automated review summary (run 1)
Per-agent results
| Agent | must fix | suggestion | could fix | future work | outstanding | Verdict |
|---|---|---|---|---|---|---|
| Security Vulnerabilities | 0 | 0 | 1 | 0 | 1 | Go |
| Supply Chain / Runner Safety | 0 | 0 | 0 | 0 | 0 | Go |
| F Prime C/C++ Design | 1 | 1 | 1 | 0 | 3 | No-Go |
| Documentation Currency | 1 | 2 | 0 | 0 | 3 | No-Go |
| Design | 0 | 0 | 1 | 0 | 1 | Go |
| Architecture | 0 | 0 | 0 | 0 | 0 | Go |
| Test Quality | 1 | 2 | 0 | 0 | 3 | No-Go |
| CI safety | — | — | — | — | — | Go |
| Totals | 3 | 5 | 3 | 0 | 11 | No-Go |
Supply-chain surfaces
| Surface | Outstanding |
|---|---|
| Dependencies | clean |
| Vendored / submodule | clean |
| Build / test infrastructure | clean |
| Workflows / actions / scripts | clean |
| Generator output | clean |
| Prompt-injection | clean |
| Review-system integrity | clean |
Outstanding must-fix items (3)
F Prime C/C++ Design
- CPP-17:
SandboxedFilemissing explicit= deletefor copy/move operations —Os/SandboxedFile.hpp
Documentation Currency
- Os SDD (
Os/docs/sdd.md) missing entries for newFilePathUtilsandSandboxedFileutilities —Os/FilePathUtils.hpp
Test Quality
checkContainment()public API has no dedicated unit tests —Os/test/ut/FilePathUtilsTest.cpp
Merge readiness
Merge readiness: No-Go — 3 outstanding must-fix items across C++ Design (1), Documentation Currency (1), and Test Quality (1).
Agents that did not run on this PR
- (none — all 7 reviewers invoked successfully)
Well-structured PR with thorough path-resolution logic and solid test coverage. The three must-fix items are straightforward to address: add explicit copy/move deletion, update the Os SDD, and add dedicated checkContainment tests.
19b1f2c to
85393ee
Compare
thomas-bc
left a comment
There was a problem hiding this comment.
A few recommendations
LeStarch
left a comment
There was a problem hiding this comment.
Looks reasonable to me!
…ed file access Introduces a reusable composition wrapper around Os::File that restricts file operations to a configured allowed directory. This prevents path traversal attacks from components that accept externally supplied file paths. New components: - Os::FilePathValidator: standalone utility for path resolution (collapsing . and .. segments) and directory containment checking - Os::SandboxedFile: wrapper that validates paths on open() before delegating to Os::File, returning NO_PERMISSION for paths outside the sandbox Updates FileUplink to use Os::SandboxedFile with a configureSandbox() method, demonstrating the opt-in pattern for existing components. Includes 35 unit tests covering path resolution, containment checks, traversal attacks, and sandboxed file I/O. Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Address Devin Review feedback: - SandboxedFile::open() now delegates to Os::File when unconfigured, so existing deployments are not broken by the swap to SandboxedFile. Sandboxing activates only after configure() is called. - Use FW_ASSERT on getcwd() return value in FileUplinkTester to avoid undefined behavior if getcwd fails. Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
… FW_ASSERT in configure, require configure before open, use Fw::FileNameString, split tests, use for loops Changes per review: - Rename FilePathValidator -> FilePathUtils (comment 27) - Rename checkContainment -> isSubDirectory with internal resolve (comment 28) - Rename configureSandbox -> configure in FileUplink (comment 23) - Use Fw::FileNameString for m_allowedDirectory storage (comment 24) - Replace if-checks with FW_ASSERT in configure() (comment 25) - Require configure() before open(), return NO_PERMISSION if unconfigured (comment 26) - Split tests into FilePathUtilsTest.cpp and SandboxedFileTest.cpp (comment 29) - Use for loops instead of while loops (comment 30) - Refactor resolvePath into smaller helpers (buildWorkBuf, resolveSegments) Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Replace two-buffer approach (workBuf + PathSegment array + reconstruction) with in-place resolution using read/write pointers on resolvedOut. The write pointer never overtakes the read pointer (resolution only shrinks), so memmove is safe. Eliminates ~120-entry PathSegment stack array and the separate workBuf. Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
…, validation hardening - Resolve relative paths against CWD (Os::FileSystem::getWorkingDirectory) instead of allowed directory in isSubDirectory - Add Fw::StringBase overloads for resolvePath and isSubDirectory - Validate string_length != buffer_length (reject unterminated strings) - Add TOO_LONG status for buffer overflow (distinct from INVALID_PATH) - Use descriptive variable names (readPos/writePos instead of r/w) - Add explanatory comments in resolveInPlace algorithm - Use parentheses around arithmetic in buffer size check - Verify baseDir length > 0 before indexing - Remove resolvedOut parameter from isSubDirectory (callers resolve separately) - Clarify checkContainment edge case where path == allowedDir sans trailing / Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
…tead of nullptr, scope cwdBuffer - resolvePath(StringBase): write directly into resolvedOut's internal buffer via const_cast (safe because resolvePath always null-terminates) - isSubDirectory: pass "/" instead of nullptr for allowedDirectory normalization - Move cwdBuffer allocation inside the if block (only when path is relative) Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
… SandboxedFile::open - Make checkContainment a public function in FilePathUtils namespace - SandboxedFile::open now calls resolvePath + checkContainment directly instead of going through isSubDirectory (which would resolve again internally) - Saves one MAX_PATH_LENGTH stack buffer and one redundant resolve pass Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Prevents mismatch where getCapacity() (MAX_PATH_LENGTH+1) could let buildAbsolutePath accept a path that resolveInPlace then rejects because its string_length scan is bounded to MAX_PATH_LENGTH. Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
…pen paths against CWD - configure() now resolves allowedDirectory (relative paths use CWD as base) and asserts resolution succeeds. Trailing '/' appended automatically. - open() resolves file paths against CWD instead of the sandbox directory, matching standard POSIX semantics for relative paths. - Removes requirement that allowedDirectory be absolute — relative dirs are resolved at configure time. Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
…ttern - Add FilePathUtils::resolveFromCwd() that handles the CWD lookup for relative paths in a single call - Refactor isSubDirectory, SandboxedFile::configure, and SandboxedFile::open to use resolveFromCwd instead of duplicating the CWD-fetch-then-resolve logic Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
isSubDirectory resolved paths internally and checked containment. Now that SandboxedFile::open uses resolveFromCwd + checkContainment directly, isSubDirectory has no callers. Removed both overloads and all 15 tests. Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
…emoval - Delete copy/move constructors on SandboxedFile (rule of three/five) - Add 8 dedicated checkContainment() unit tests covering prefix boundaries, root sandbox, empty strings, and subdirectory containment - Add 2 resolveFromCwd() unit tests (absolute passthrough, relative resolution) - Add 2 SandboxedFile edge-case tests (empty path, overlong path) - Replace const_cast in StringBase overload with local buffer + assignment - Replace magic 256 with MAX_PATH_LENGTH in FileUplinkTester - Update Os SDD to include FilePathUtils and SandboxedFile entries - Update FileUplink SDD to document sandbox configure() behavior - Document symlink threat model assumption in SandboxedFile class comment Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
- Add Os::FileInterface::OUTSIDE_SANDBOX status to distinguish sandbox
rejection from generic NO_PERMISSION errors. Update both the C++ enum
and the FPP shadow enum (FileStatus) with static_assert consistency.
- SandboxedFile::open now returns OUTSIDE_SANDBOX instead of
NO_PERMISSION when a path fails containment or resolution checks.
- Configure FileUplink sandbox in Ref deployment via FPP configComponents
phase: FileHandling::fileUplink.configure("/tmp/uplink/").
- Add test_file_uplink_sandbox integration test in Ref that verifies:
1. Uplink to /tmp/evil/escaped.bin (outside sandbox) -> FileOpenError
2. Uplink to /tmp/uplink/test.bin (inside sandbox) -> FileReceived
Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
5c996bf to
72d4a85
Compare
- Per review, move FileUplink sandbox configuration out of the shared FileHandling subtopology (which would force it on all deployments) and into Ref's RefTopology.cpp configureTopology(), leaving the sandbox directory choice up to each deployment. - Fix clang-format include ordering in FilePathUtilsTest.cpp and FileUplinkTester.cpp. - Fix black formatting in ref_integration_test.py. Co-Authored-By: michael.d.starch <michael.d.starch@jpl.nasa.gov>
…ectory-sandboxed file access (#5320)
Change Description
Adds two new Os-layer utilities for directory-sandboxed file access:
Os::FilePathUtils— standalone namespace for textual path resolution and containment checking:resolvePath(path, baseDir, out, size)— resolves.,..,//in-place via single-pass read/write pointer algorithm. No heap, no I/O.resolveFromCwd(path, out, size)— convenience wrapper resolving relative paths against CWD.checkContainment(resolvedPath, allowedDir)— prefix-based containment check on already-resolved paths.Os::SandboxedFile— composition wrapper aroundOs::File:configure(dir)resolves the allowed directory at configuration time (relative paths resolved against CWD). Asserts on failure. Trailing/appended automatically.open(path, ...)resolves file paths against CWD, checks containment, then delegates toOs::File::open()."/"(all paths allowed) for backward compatibility.Svc::FileUplinkupdated as opt-in example withconfigureSandbox()method.Rationale
Multiple F Prime components (FileUplink, CFDP receiver) need to restrict file access to approved directories. Without sandboxing, an uplinked file packet with a path like
../../etc/shadowcould write anywhere on the filesystem. This provides a reusable, flight-grade mechanism — any component can adopt it by swappingOs::File→Os::SandboxedFile.Testing/Review Recommendations
Os_FilePathUtils_Test: 14 tests covering path resolution edge cases (.,..,//, buffer overflow, empty segments, root traversal, relative paths, StringBase overloads)Os_SandboxedFile_Test: 7 tests covering sandbox enforcement, configure/reconfigure, default behavior, traversal rejectionFuture Work
realpath()integration for symlink resolution on POSIX targetsAI Usage (see policy)
This PR was authored by Devin (AI software engineer by Cognition AI) with iterative code review and design direction from @LeStarch. AI was used for: code generation, test writing, documentation, and addressing review feedback across multiple review rounds.
IAMAI