Skip to content

Bind external data validation to opened files - #33063

Merged
Akshay Sonawane (apsonawane) merged 9 commits into
mainfrom
fix/external-data-open-validation
Oct 6, 2026
Merged

Akshay Sonawane (apsonawane) merged 9 commits into
mainfrom
fix/external-data-open-validation

Conversation

@apsonawane

Copy link
Copy Markdown
Contributor

This pull request enhances the security and robustness of external data file handling in ONNX Runtime by improving how external files are accessed, validated, and mapped into memory. The changes introduce canonical path and memory-mapping support to the RandomAccessFile abstraction, update file access logic to use open file handles rather than paths, and add stricter validation to prevent directory traversal attacks. Additionally, platform-specific implementations and tests have been updated to support these improvements.

Security and Path Validation Enhancements:

  • Added ValidateOpenedExternalDataPath to ensure that opened external data files do not escape the model directory, mitigating directory traversal vulnerabilities. This validation is now called after opening external data files and before reading or mapping their contents. [1] [2] [3]

RandomAccessFile API Extensions:

  • Extended the RandomAccessFile interface with GetCanonicalPath and Map methods, enabling platform-independent retrieval of the canonical file path and memory-mapping of file regions.
  • Implemented these methods for POSIX (PosixRandomAccessFile) and Windows (WindowsRandomAccessFile) platforms, including robust error handling and platform-specific path normalization. [1] [2]

Refactoring of File Access Logic:

  • Updated all external data file reading and prepacked weight loading logic to use the RandomAccessFile abstraction for reads and memory mapping, instead of repeatedly opening files by path. This ensures consistency and prevents issues if the file path is changed after opening. [1] [2] [3] [4] [5] [6] [7]

Testing Improvements:

  • Added a new test to verify that memory mapping and canonical path retrieval operate correctly even if the file path is replaced after the file is opened, ensuring the API behaves as documented.

Platform and Build Enhancements:

  • Included necessary headers and improved platform compatibility for new features, such as memory mapping and canonical path retrieval.

These changes collectively make external data handling more secure, reliable, and portable across platforms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

AppContainer and native POSIX regressions remain, and device-loader reads still bypass handle-bound validation.

Review effort: Balanced
Findings: 3 High severity · 1 Low severity

Open (4)
What changed in this PR

Strengthens external-data handling by binding validation and reads to opened file handles.

Changes:

  • Extends RandomAccessFile with canonical-path and memory-mapping APIs.
  • Adds POSIX and Windows implementations.
  • Validates opened external files and tests path-replacement behavior.
File Description
onnxruntime/​core/​platform/​env.h Extends the random-access file interface.
onnxruntime/​core/​platform/​posix/​env.cc Implements POSIX canonical paths and mapping.
onnxruntime/​core/​platform/​windows/​env.cc Implements Windows canonical paths and mapping.
onnxruntime/​core/​framework/​tensorprotoutils.cc Uses opened handles for validation and loading.
onnxruntime/​test/​platform/​env_test.cc Tests mapping after pathname replacement.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread onnxruntime/core/platform/posix/env.cc Resolved
Comment thread onnxruntime/test/platform/env_test.cc Resolved
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@github-actions github-actions Bot 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.

You can commit the suggested changes from lintrunner.

Comment thread onnxruntime/core/platform/posix/env.cc Resolved
Comment thread onnxruntime/core/platform/posix/env.cc Resolved

Copilot AI 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.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 1 High severity · 2 Medium severity · 1 Low severity

Open (4)
Resolved since last review (4)

Comment thread onnxruntime/core/platform/posix/env.cc Resolved
Comment thread onnxruntime/core/platform/env.h Resolved
Comment thread onnxruntime/core/platform/posix/env.cc Resolved
Use read-only handle mappings with checked page sizes and lightweight deleters, update CUDA tests for the handle-based loader API, and account for the measured Android binary growth.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wrap the lightweight RandomAccessFile mapping deleter in the std::function required by IAllocatorUniquePtr before releasing mapped-memory ownership.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@titaiwangms Ti-Tai Wang (titaiwangms) 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.

Binding validation and reads to one opened file is a useful improvement, but the new CPU mapping path introduces portability regressions and the test loader no longer matches the retained WASM interface. Please address the three inline findings. These findings are grounded in the pinned source/platform contracts; they were not locally runtime-tested, and the WASM checks were still queued when inspected.

Review exclusions/open questions: accessible procfs is now required by the Linux canonical-path query; the fallback canonicalization and Windows long-path/UNC/AppContainer behavior need platform coverage. In-place filesystem mutation remains outside the abstraction's documented guarantees. No exhaustive security audit or performance claim is implied.

Comment thread onnxruntime/core/platform/posix/env.cc Resolved

@tianleiwu Tianlei Wu (tianleiwu) 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.

The same-opened-object map/read/provider routing is an improvement, and the earlier CUDA-loader bypass is fixed. The generic POSIX fallback still does not establish a race-safe canonical path; I continued the portability thread with the concrete symlink interleaving. The existing read-only big-endian mapping and 32-bit backing-file concerns remain open, and current WASM Debug compiler evidence confirms the existing test-signature blocker. One additional Linux deployment regression is inline. Reviewed all 12 changed files and nearby platform/loader consumers; no local execution. Other pre-existing mutable-root/typed-unpack hardening gaps are not claimed as new regressions.

Comment thread onnxruntime/core/platform/posix/env.cc Resolved
Use wide opened-file lengths, retain writable private POSIX mappings, provide the WASM loader signature, and reject platforms without secure handle-backed canonicalization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@titaiwangms Ti-Tai Wang (titaiwangms) 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.

All three previous findings are addressed in source: POSIX mappings are private/writable for byte swapping, file lengths/range metadata use uint64_t, and the tracking loader has matching WASM/native signatures. One remaining platform-test blocker becomes explicit with the new fail-closed canonicalization fallback: the new opened-file mapping test is still unconditional on WASM, where GetCanonicalPath is unsupported.

This finding is grounded in the pinned test, platform implementation, CMake test selection, and a passive clang Emscripten-target macro probe; no local ORT/WASM test was run and the current WASM job is still pending. Linux still intentionally requires procfs, and unimplemented POSIX canonicalization now fails closed; runtime coverage for big-endian, Win32 large backing files, and Windows path variants remains excluded.

Comment thread onnxruntime/test/platform/env_test.cc Resolved
Describe procfs and supported handle-backed canonicalization platforms, and keep WASM mapping coverage without native path assertions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@tianleiwu Tianlei Wu (tianleiwu) 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.

Re-reviewed all 13 changed files at aaee5dc. No remaining actionable findings. The loading guide now explicitly documents the native POSIX fail-closed restriction and Linux/Android procfs requirement, and the mapping test excludes native canonicalization under WASM. The same-opened-file CPU/prepacked/CUDA routing, wide length metadata, writable private POSIX mapping, and Windows AppContainer fallback address the earlier concerns. Source review only; no local build/runtime verification. WASM Release static-library and React Native iOS E2E checks were still pending at review time; please let CI complete before merging.

@tianleiwu Tianlei Wu (tianleiwu) 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.

Re-reviewed the current head. The merge of main does not change the PR's own files, and all earlier review threads are resolved. I found no remaining correctness or security concern in the handle-bound validation (open, canonical path, containment check, then read or map on the same handle).

One CI item remains: AndroidBinarySizeCheckJob_MinimalBaseline fails on this head (Sections total size ... of 1604334 exceeds threshold of 1601536 by 2798). The same job passed at aaee5dce, so the PR now consumes the remaining headroom on top of current main. Please either reduce the added code in the minimal baseline (see the inline suggestion) or raise BINARY_SIZE_THRESHOLD_IN_BYTES in .github/workflows/android.yml with a note that the growth comes from the opened-file validation. build_x64_release and webgpu_compile_x64_RelWithDebInfo (vcpkg) were still running when I checked, so please confirm they finish green.

@titaiwangms Ti-Tai Wang (titaiwangms) 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.

The previous WASM test blocker is addressed: the native GetCanonicalPath assertion is now excluded on WASM while the mapping/read coverage remains. Reviewed the shared OpenValidatedExternalDataFile extraction at all three native entry points; it retains validation of the opened handle before access. The new documentation explicitly states procfs and unsupported-POSIX fail-closed requirements. No actionable introduced blocker found in this revision.

Required CI is not yet green: the current-head Android minimal size check measured 1,604,278 bytes against a 1,601,536-byte threshold (job 111969099912), a 2,742-byte excess. Please resolve the size gate with measured justification; this approval does not waive it. Other checks remain pending. Static incremental review; no local tests, big-endian/Win32 runtime checks, or exhaustive filesystem-race testing.

@apsonawane
Akshay Sonawane (apsonawane) deleted the fix/external-data-open-validation branch October 6, 2026 00:27
Tianlei Wu (tianleiwu) added a commit that referenced this pull request Nov 13, 2026
…34099)

### Description

Since `huggingface_hub` 1.32, the Hugging Face Hub cache stores Xet
files once per cache in a [blob store shared across
repos](https://huggingface.co/docs/huggingface_hub/guides/manage-cache#shared-blobs-across-repos)
(`<cache>/blobs/<hash prefix>/<hash>`), and the repo's `blobs/<etag>`
becomes a relative symlink to it. A model and its external data in one
snapshot can then resolve into different folders (for example
`blobs/28/…` and `blobs/d4/…`, or one in the repo's `blobs/` and one in
the shared store). The symlink fallback from #27374 only accepts
external data next to the resolved model, so models with external data
loaded from a default `hf_hub_download` path fail with `External data
path escapes model directory`. This PR teaches both external data
validators about the shared store without trusting anything outside the
cache's blob folders.

### Summary of Changes

#### Validation (`onnxruntime/core/framework/tensorprotoutils.cc`)

New Pass 3 in `ValidateExternalDataPath`, reached only when the model is
a symlink and Passes 1–2 fail. If the canonical model directory is in a
snapshot of a cache repo (`<cache>/<type>s--<repo
id>/snapshots/<revision>/…`, also from a subfolder such as `onnx/`),
external data is accepted when:

1. its path does not leave the model directory before symlinks are
resolved (no `..` escape), and
2. both the model and the external data resolve to files under the blob
stores of that cache: the repo's `blobs/` folder or the cache-wide
`blobs/` folder.

`ValidateOpenedExternalDataPath` applies the same blob-store check to
the canonical path of the opened file, so the opened-file validation
from #33063 also covers the new case. The cost is one extra
canonicalization, and only after the existing passes fail.

| File | Change |
|------|--------|
| `onnxruntime/core/framework/tensorprotoutils.cc` | Add
`IsInHuggingFaceCacheBlobStores` and use it in both validators. |
| `onnxruntime/test/framework/tensorutils_test.cc` |
`HuggingFaceCachePathValidationTest`: 11 tests on a cache built like
`huggingface_hub` does, with two-hop relative symlinks (6 accepted
layouts, 5 rejected). |
| `onnxruntime/test/python/onnxruntime_test_python_symlink_data.py` |
End-to-end `InferenceSession` test for the shared store and for a mixed
repo-local/shared layout. |

#### Security notes

- Paths are compared in canonical form. A canonical path has no
symlinks, so a file whose canonical path is under `<cache>/blobs` is
stored in that real folder. If a `blobs/` folder is itself a symlink or
junction, nothing matches (`huggingface_hub` also never uses a symlinked
`blobs/` folder as the shared store).
- The model itself must resolve into the blob stores. This rejects
symlinked models that point outside the cache, and `symlink/..` tricks
in the model path.
- The no-`..` requirement keeps a malicious model from reaching other
cached repos' files. In a real cache, the entries of a snapshot folder
are symlinks that `huggingface_hub` creates to files of the same repo.
- Someone who can write into the cache root gains nothing new. With the
existing Pass 2 (for example ORT 1.31.1), they can already make a model
read any blob by putting the model file in the same shard folder
(verified). `huggingface_hub` also gives the store and shard folders the
same permissions as the cache root.
- Unchanged: empty, absolute, and in-memory-tag locations are still
rejected before any fallback, and Passes 1–2 behave as before.

#### Comparison with #33974

#33974 targets the same issue. It also checks the
`.huggingface-shared-blobs` marker (version `1`) and blob names (hex,
40/64 characters, 2-character shard), and requires a `models--` repo
folder. These checks are not security boundaries, since anyone who can
create the layout can also create the marker and names. They do tie ORT
to `huggingface_hub` internals, so a future layout change would break
loading again. I built both and loaded the same synthetic caches:

| Scenario | #33974 | This PR |
|---|---|---|
| Shared store, model and data in different shards | loads | loads |
| Mixed repo-local/shared blobs (both directions) | loads | loads |
| `datasets--…` repo | fails | loads |
| Model folder reached through a symlinked directory | fails | loads |
| Shared store marker version other than `1` | fails | loads |
| Location `../x`; data symlink outside the cache; symlinked `blobs/` |
rejected | rejected |

### Motivation and Context

Fixes #33950. Downstream projects currently work around it by pinning
`huggingface_hub<=1.31`, setting `HF_HUB_DISABLE_SHARED_BLOBS=1`, or
copying files out of the cache.

### Testing

- Real cache: reproduced the issue with `onnxruntime==1.31.1` and
`huggingface_hub==2.4.0` (`onnx-community/sam2.1-hiera-tiny-ONNX`, fresh
`HF_HOME`). With this PR, the same cache loads. The two mixed layouts
created by downloading one of the files with
`HF_HUB_DISABLE_SHARED_BLOBS=1` also fail with 1.31.1 and load with this
PR.
- `onnxruntime_test_all
--gtest_filter='HuggingFaceCachePathValidationTest.*:PathValidationTest.*:GetExtDataFromTensorProtoTest.*'`:
all pass. Without the `tensorprotoutils.cc` change, the 6 tests for
accepted layouts fail. Removing any one of the conditions (no `..`
escape, model in a blob store, `blobs/` folder bound, repo folder name,
symlinked `blobs/`, opened-file check) makes the targeted test fail.
- `python -m pytest
onnxruntime/test/python/onnxruntime_test_python_symlink_data.py`: 4
passed (plus 2 subtests).
- Full `onnxruntime_test_all` on Linux x64 (CPU, Release): 2897 passed.
`lintrunner`: no issues.
- Not run locally: Windows and WASM. The new tests skip when symlinks
cannot be created, and the load test is excluded on WASM like the other
external-file tests in that file.

### Checklist

- [x] Tests added/updated
- [x] No breaking changes
- [ ] Documentation updated (not applicable)

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants