openat2 for Embedded File Imports: Keep Path Resolution Inside the Chosen Directory

A file importer that joins a chosen directory with an untrusted relative name still needs a path-resolution policy. Checking the string first and opening it later leaves filesystem traversal to a separate operation, during which links or directory entries may change.

openat2() lets the application ask the kernel to enforce specified resolution restrictions during the open. This review uses the inspected Linux man-pages 6.19 openat2 reference, read on 2026-10-19. The worked flags date to the interface introduced in Linux 5.6 and are suitable for reviewing a v6.12-era target; newer options on the live page are excluded.

Choose the meaning of an absolute name

Assume an application already holds an authorized directory descriptor for a synthetic import root. Its acquisition, identity and permission policy are prerequisites, not solved by adding a resolve flag later.

RESOLVE_BENEATH requires resolution to remain below that directory and rejects absolute pathnames and absolute symlinks. It fits a contract in which callers must supply relative names within the chosen import tree.

RESOLVE_IN_ROOT instead treats the directory as the root for that particular lookup, including absolute pathnames and absolute symlink targets. It changes the interpretation of a supplied absolute name rather than rejecting the name merely because it is absolute. This is a per-open resolution behavior, not a permanent process-root change.

Choose one of those contracts deliberately. A recovery path that silently switches between them changes the caller’s requested interpretation. The synthetic importer below chooses BENEATH and rejects absolute input rather than reinterpreting it.

BENEATH rejects absolute input while IN_ROOT interprets it under the supplied directory; the strict example adds all-component symlink rejection.
Original resolution-policy diagram; no syscall fixture executed.

For this import format, assume symlinks are deliberately forbidden at every component. RESOLVE_NO_SYMLINKS enforces that resolution policy and also implies RESOLVE_NO_MAGICLINKS. By contrast, ordinary O_NOFOLLOW concerns the final pathname component; it does not provide the same all-components rule.

If another product intentionally permits ordinary symlinks, review RESOLVE_NO_MAGICLINKS explicitly instead of relying on a current side effect of the containment flags. Magic links, including certain procfs references, have distinct resolution behavior.

A conceptual setup fragment for the strict relative-name contract is:

struct open_how how = {0};
how.flags = O_RDONLY | O_CLOEXEC;
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS;
/* Pass the established root descriptor, supplied name, &how,
   and sizeof(how) to the reviewed openat2 platform wrapper. */

This is not a complete importer or an executed example. Required headers, syscall availability, errors, descriptor lifetime and object-type policy belong to the implementation. Zero initialization preserves the API’s extensibility contract and avoids unintended nonzero fields.

Use names that expose the policy differences

With an established root and a synthetic existing file batch/report.bin, the relative name is a candidate for acceptance. A name ../outside.bin attempts to cross the chosen boundary. An absolute /batch/report.bin is rejected under the chosen BENEATH policy even though IN_ROOT would interpret it inside the supplied root.

Add an intermediate symlink such as incoming/latest/report.bin to the fixture. Under the strict no-symlink contract, resolution through that link is rejected even if its target happens to remain inside the tree. That case tests the link policy rather than containment alone.

These are expected policy outcomes, not observed host syscall results. Keep the synthetic files harmless and owned by the test environment when implementing such a fixture.

Preserve the policy on errors

The documented containment checks can return EAGAIN when the kernel cannot establish that a race involving traversal stayed within the boundary. A bounded retry may use the same intended restrictions. It must not remove the restrictions merely to turn a failure into success.

EXDEV can identify a forbidden boundary crossing. Other flags can use the same error for different constraints, so decode errors with the actual resolve mask. Unsupported or invalid configurations need a qualified compatibility decision. An ordinary open() fallback does not preserve this contract automatically.

RESOLVE_CACHED is not used here. Its separate cache-only EAGAIN meaning should not be confused with the containment-race case if an implementation later adds that flag.

Keep content admission outside the containment claim

A contained pathname does not authenticate its contents or stop another writer from changing an opened file. It also does not guarantee a regular file, harmless open-time behavior, a maximum size or a bounded read. The fragment therefore cannot be used as a complete safe-file admission recipe, especially in a tree containing device nodes or FIFOs.

Mount traversal is another explicit choice: RESOLVE_NO_XDEV can prohibit crossing mount points, including bind mounts, but it is not part of this example’s policy. The import root’s mount layout must match the intended trust model.

The final review artifact should record root acquisition, pathname interpretation, link and mount rules, error handling, object admission and content validation separately. Path containment becomes useful when its exact guarantee is preserved, rather than expanded into a claim of complete process confinement.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *