Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/Model_Loading_Performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ for external tensors of at least 16 MiB; smaller tensors use one read. The optim
device and filesystem. If pinned buffers or CUDA streams cannot be created, loading falls back to a pageable buffer.
Models with weights embedded in the ONNX file do not use this external-data path.

### Opened-file path validation requirements

For native models that use external data, ONNX Runtime validates the canonical path of the opened file handle before
reading or mapping it. This prevents a pathname or symlink from being swapped between validation and access.

- On Linux and Android, `/proc/self/fd` must be available. Containers, chroots, and sandboxes must mount procfs so
ONNX Runtime can resolve the opened handle securely.
- Apple platforms use `F_GETPATH`, FreeBSD uses `F_KINFO`, and Windows uses
`GetFinalPathNameByHandle`.
- Other native POSIX platforms without a secure handle-backed canonical-path API, including the current NetBSD and
AIX implementations, fail closed when loading external-data models. Embedded-data models remain supported.
- WebAssembly does not provide native canonical-path validation. Its external-data loader uses the environment/path
interface instead of the native opened-file path.

ONNX Runtime intentionally does not fall back to resolving and comparing a mutable pathname because that would
reintroduce a time-of-check/time-of-use race.

Configure the CUDA EP in Python:

```python
Expand Down
9 changes: 9 additions & 0 deletions onnxruntime/core/framework/external_data_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,22 @@ class IExternalDataLoader {
virtual bool CanLoad(const OrtMemoryInfo& target_memory_info) const = 0;

// Tensor should be already allocated with the correct memory info and size.
#if defined(__wasm__)
virtual common::Status LoadTensor([[maybe_unused]] const Env& env,
[[maybe_unused]] const std::filesystem::path& data_file_path,
[[maybe_unused]] FileOffsetType data_offset,
[[maybe_unused]] SafeInt<size_t> data_length,
[[maybe_unused]] Tensor& tensor) const {
ORT_NOT_IMPLEMENTED(__FUNCTION__, " is not implemented");
}
#else
virtual common::Status LoadTensor([[maybe_unused]] const RandomAccessFile& file,
[[maybe_unused]] FileOffsetType data_offset,
[[maybe_unused]] SafeInt<size_t> data_length,
[[maybe_unused]] Tensor& tensor) const {
ORT_NOT_IMPLEMENTED(__FUNCTION__, " is not implemented");
}
#endif
};
Comment thread
apsonawane marked this conversation as resolved.
Resolved

#if defined(__wasm__)
Expand Down
93 changes: 71 additions & 22 deletions onnxruntime/core/framework/tensorprotoutils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,34 @@ Status ValidateExternalDataPath(const std::filesystem::path& model_path,
external_data_canonical, " ", "allowed directory: ", real_model_dir);
}

[[maybe_unused]] static Status ValidateOpenedExternalDataPath(const std::filesystem::path& model_path,
const std::filesystem::path& opened_path) {
const std::filesystem::path model_dir = model_path.empty() || model_path.parent_path().empty()
? std::filesystem::path{"."}
: model_path.parent_path();

std::filesystem::path model_dir_canonical;
ORT_RETURN_IF_ERROR(WeaklyCanonicalPath(model_dir, model_dir_canonical));
if (HasPathComponentPrefix(model_dir_canonical, opened_path)) {
return Status::OK();
}

std::error_code ec;
if (!model_path.empty() && std::filesystem::is_symlink(model_path, ec)) {
std::filesystem::path real_model_path;
ORT_RETURN_IF_ERROR(WeaklyCanonicalPath(model_path, real_model_path));
std::filesystem::path real_model_dir_canonical;
ORT_RETURN_IF_ERROR(WeaklyCanonicalPath(real_model_path.parent_path(), real_model_dir_canonical));
if (HasPathComponentPrefix(real_model_dir_canonical, opened_path)) {
return Status::OK();
}
}

return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL,
"Opened external data file escapes model directory. Opened path: ",
opened_path, " allowed directory: ", model_dir_canonical);
}

Status GetExternalDataInfo(const ONNX_NAMESPACE::TensorProto& tensor_proto,
const std::filesystem::path& tensor_proto_dir,
std::basic_string<ORTCHAR_T>& external_file_path,
Expand Down Expand Up @@ -1624,30 +1652,35 @@ ORT_API(void, OrtUninitializeBuffer, _In_opt_ void* input, size_t input_len, enu
#endif

#if !defined(__wasm__)
static Status GetFileContent(const Env& env, const std::filesystem::path& file_path, FileOffsetType offset,
static Status GetFileContent(const RandomAccessFile& file, FileOffsetType offset,
size_t length, IAllocatorUniquePtr<void>& external_data) {
// query length if it is 0
if (length == 0) {
// The return type of std::filesystem::file_size is uintmax_t which could be bigger than size_t
length = narrow<size_t>(std::filesystem::file_size(file_path));
uint64_t file_length = 0;
ORT_RETURN_IF_ERROR(file.GetLength(file_length));
length = SafeInt<size_t>(file_length);
}

// first, try to map into memory
{
Env::MappedMemoryPtr mapped_memory{};
auto status = env.MapFileIntoMemory(file_path.native().c_str(), offset, length, mapped_memory);
RandomAccessFile::MappedMemoryPtr mapped_memory{};
auto status = file.Map(offset, length, mapped_memory);
if (status.IsOK()) {
IAllocatorUniquePtr<void> raw_buffer(mapped_memory.release(),
mapped_memory.get_deleter());
const auto mapped_memory_deleter = mapped_memory.get_deleter();
IAllocatorUniquePtr<void>::deleter_type raw_buffer_deleter =
[mapped_memory_deleter](void* p) {
mapped_memory_deleter(static_cast<char*>(p));
};
auto* mapped_data = mapped_memory.release();
IAllocatorUniquePtr<void> raw_buffer(mapped_data, std::move(raw_buffer_deleter));
external_data.swap(raw_buffer);
return Status::OK();
}
}

// if that fails, try to copy
auto buffer = std::make_unique<char[]>(length);
ORT_RETURN_IF_ERROR(
env.ReadFileIntoBuffer(file_path.native().c_str(), offset, length, gsl::make_span(buffer.get(), length)));
ORT_RETURN_IF_ERROR(file.Read(offset, gsl::make_span(buffer.get(), length)));

IAllocatorUniquePtr<void> raw_buffer(buffer.release(), [](void* p) { delete[] reinterpret_cast<char*>(p); });
external_data.swap(raw_buffer);
Expand All @@ -1673,8 +1706,7 @@ static Status ValidateExternalFilePathForTensor(const ONNX_NAMESPACE::TensorProt
}

#if !defined(__wasm__)
static Status LoadPrepackedWeightsFromFile(const Env& env,
const std::filesystem::path& external_data_file_path,
static Status LoadPrepackedWeightsFromFile(const RandomAccessFile& file,
std::uintmax_t file_length,
const ExternalDataInfo::PrepackedInfos& prepacked_infos,
PrepackedWeightsForGraph& prepacked_info) {
Expand All @@ -1692,8 +1724,7 @@ static Status LoadPrepackedWeightsFromFile(const Env& env,
" is out of bounds and can not read in full");

IAllocatorUniquePtr<void> data_ptr;
ORT_RETURN_IF_ERROR(GetFileContent(env, external_data_file_path, blob_offset, blob_length,
data_ptr));
ORT_RETURN_IF_ERROR(GetFileContent(file, blob_offset, blob_length, data_ptr));
prepacked_weights.buffers_.push_back(std::move(data_ptr));
prepacked_weights.buffer_sizes_.push_back(blob_length);
}
Expand Down Expand Up @@ -1805,9 +1836,14 @@ Status GetExtDataFromTensorProto(const Env& env,
buffer.release();

#else
// The GetFileContent function doesn't report error if the requested data range is invalid. Therefore we need to
// manually check file size first.
std::uintmax_t file_length = std::filesystem::file_size(external_data_file_path);
std::unique_ptr<RandomAccessFile> external_data_file;
ORT_RETURN_IF_ERROR(env.OpenRandomAccessFile(external_data_file_path.c_str(), external_data_file));
PathString opened_path;
ORT_RETURN_IF_ERROR(external_data_file->GetCanonicalPath(opened_path));
ORT_RETURN_IF_ERROR(ValidateOpenedExternalDataPath(model_path, std::filesystem::path{opened_path}));
Comment thread
apsonawane marked this conversation as resolved.
Resolved

uint64_t file_length = 0;
ORT_RETURN_IF_ERROR(external_data_file->GetLength(file_length));
Comment thread
tianleiwu marked this conversation as resolved.
Resolved

SafeInt<FileOffsetType> end_of_read(file_offset);
end_of_read += raw_data_safe_len;
Expand All @@ -1817,8 +1853,7 @@ Status GetExtDataFromTensorProto(const Env& env,
" are out of bounds or can not be read in full.");

IAllocatorUniquePtr<void> ext_data_buf;
ORT_RETURN_IF_ERROR(GetFileContent(env, external_data_file_path, file_offset, raw_data_safe_len,
ext_data_buf));
ORT_RETURN_IF_ERROR(GetFileContent(*external_data_file, file_offset, raw_data_safe_len, ext_data_buf));

// Data on disk is little endian
if constexpr (endian::native != endian::little) {
Expand Down Expand Up @@ -1847,7 +1882,7 @@ Status GetExtDataFromTensorProto(const Env& env,

if (prepacked_info != nullptr && !prepacked_infos->empty()) {
ORT_RETURN_IF_ERROR(LoadPrepackedWeightsFromFile(
env, external_data_file_path, file_length, *prepacked_infos, *prepacked_info));
*external_data_file, file_length, *prepacked_infos, *prepacked_info));
}
#endif
}
Expand Down Expand Up @@ -1885,9 +1920,14 @@ Status LoadPrepackedWeightsFromExternalData(const Env& env,
ORT_RETURN_IF(external_data_file_path == kTensorProtoNativeEndianMemoryAddressTag ||
external_data_file_path == kTensorProtoLittleEndianMemoryAddressTag,
"Pre-packed blobs cannot be restored from an in-memory external tensor.");
const std::uintmax_t file_length = std::filesystem::file_size(external_data_file_path);
return LoadPrepackedWeightsFromFile(
env, external_data_file_path, file_length, prepacked_infos, prepacked_info);
std::unique_ptr<RandomAccessFile> external_data_file;
ORT_RETURN_IF_ERROR(env.OpenRandomAccessFile(external_data_file_path.c_str(), external_data_file));
PathString opened_path;
ORT_RETURN_IF_ERROR(external_data_file->GetCanonicalPath(opened_path));
ORT_RETURN_IF_ERROR(ValidateOpenedExternalDataPath(model_path, std::filesystem::path{opened_path}));
Comment thread
apsonawane marked this conversation as resolved.
Resolved
uint64_t file_length = 0;
ORT_RETURN_IF_ERROR(external_data_file->GetLength(file_length));
return LoadPrepackedWeightsFromFile(*external_data_file, file_length, prepacked_infos, prepacked_info);
#endif
}

Expand Down Expand Up @@ -1916,7 +1956,16 @@ Status LoadExtDataToTensorFromTensorProto(const Env& env, const std::filesystem:
ORT_RETURN_IF(external_data_file_path == onnxruntime::utils::kTensorProtoLittleEndianMemoryAddressTag || external_data_file_path == onnxruntime::utils::kTensorProtoNativeEndianMemoryAddressTag,
"Memory address tag is not supported by custom external data loader.");

#if defined(__wasm__)
return ext_data_loader.LoadTensor(env, external_data_file_path, file_offset, raw_data_safe_len, tensor);
#else
std::unique_ptr<RandomAccessFile> external_data_file;
ORT_RETURN_IF_ERROR(env.OpenRandomAccessFile(external_data_file_path.c_str(), external_data_file));
PathString opened_path;
ORT_RETURN_IF_ERROR(external_data_file->GetCanonicalPath(opened_path));
ORT_RETURN_IF_ERROR(ValidateOpenedExternalDataPath(model_path, std::filesystem::path{opened_path}));
return ext_data_loader.LoadTensor(*external_data_file, file_offset, raw_data_safe_len, tensor);
#endif
}

#define CASE_PROTO(X, Y) \
Expand Down
26 changes: 24 additions & 2 deletions onnxruntime/core/platform/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,38 @@ std::pair<int, std::string> GetErrnoInfo();
/**
* An owned open file supporting concurrent positional reads.
*
* Reads and length queries refer to the same file even if its pathname is replaced.
* Reads, mappings, length queries, and canonical-path queries refer to the same file even if its pathname is replaced.
* This is not a snapshot: callers must not modify the file in place while reading it.
* Keep the object alive until all callers have finished. Its destruction closes the file.
*/
class RandomAccessFile {
public:
struct MappedMemoryDeleter {
using UnmapFn = void (*)(void* mapped_base, size_t mapped_length) noexcept;

void* mapped_base{};
size_t mapped_length{};
UnmapFn unmap{};

void operator()(char*) const noexcept {
if (unmap != nullptr) {
unmap(mapped_base, mapped_length);
}
}
};

using MappedMemoryPtr = std::unique_ptr<char[], MappedMemoryDeleter>;

virtual ~RandomAccessFile() = default;

// Query the open file, leaving length unchanged on failure.
virtual common::Status GetLength(size_t& length) const = 0;
virtual common::Status GetLength(uint64_t& length) const = 0;

// Return the canonical path of this open file handle.
virtual common::Status GetCanonicalPath(PathString& path) const = 0;

// Map bytes from this open file without reopening its pathname.
virtual common::Status Map(FileOffsetType offset, size_t length, MappedMemoryPtr& mapped_memory) const = 0;

/**
* Fill buffer starting at offset without changing a shared file position.
Expand Down
80 changes: 78 additions & 2 deletions onnxruntime/core/platform/posix/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ limitations under the License.
#endif
#include <unistd.h>

#include <array>
#include <climits>
#include <filesystem>
#include <iostream>
#include <optional>
Expand All @@ -53,6 +55,9 @@ limitations under the License.
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__)
#include <sys/sysctl.h>
#endif
#if defined(__FreeBSD__)
#include <sys/user.h>
#endif

#include "core/common/common.h"
#include <gsl/gsl>
Expand Down Expand Up @@ -135,8 +140,79 @@ class PosixRandomAccessFile final : public RandomAccessFile {
PosixRandomAccessFile(ScopedFileDescriptor descriptor, std::string path)
: descriptor_(std::move(descriptor)), path_(std::move(path)) {}

common::Status GetLength(size_t& length) const override {
return GetFileLength(descriptor_.Get(), length);
common::Status GetLength(uint64_t& length) const override {
struct stat file_stat{};
if (TempFailureRetry(fstat, descriptor_.Get(), &file_stat) < 0) {
return ReportSystemError("fstat", path_);
}
ORT_RETURN_IF(file_stat.st_size < 0, "RandomAccessFile: received negative file length.");
length = static_cast<uint64_t>(file_stat.st_size);
return Status::OK();
}

common::Status GetCanonicalPath(PathString& path) const override {
#if defined(F_GETPATH)
std::array<char, PATH_MAX> buffer{};
if (fcntl(descriptor_.Get(), F_GETPATH, buffer.data()) != 0) {
return ReportSystemError("fcntl(F_GETPATH)", path_);
}
path.assign(buffer.data());
return Status::OK();
#elif defined(__linux__) || defined(__ANDROID__)
const std::string fd_path = "/proc/self/fd/" + std::to_string(descriptor_.Get());
std::array<char, PATH_MAX> buffer{};
const auto length = readlink(fd_path.c_str(), buffer.data(), buffer.size() - 1);
if (length < 0) {
Comment thread
tianleiwu marked this conversation as resolved.
Resolved
return ORT_MAKE_STATUS(
ONNXRUNTIME, FAIL, "Secure opened-file canonicalization requires procfs at ", fd_path,
". Ensure /proc is mounted in the sandbox or chroot. Error: ", strerror(errno));
}
path.assign(buffer.data(), static_cast<size_t>(length));
return Status::OK();
#elif defined(__FreeBSD__) && defined(F_KINFO)
struct kinfo_file file_info{};
if (fcntl(descriptor_.Get(), F_KINFO, &file_info) != 0) {
return ReportSystemError("fcntl(F_KINFO)", path_);
}
path.assign(file_info.kf_path);
return Status::OK();
#else
return ORT_MAKE_STATUS(
ONNXRUNTIME, NOT_IMPLEMENTED,
"Secure canonical-path lookup for an opened file is not available on this POSIX platform.");
#endif
}

common::Status Map(FileOffsetType offset, size_t length, MappedMemoryPtr& mapped_memory) const override {
ORT_RETURN_IF_NOT(offset >= 0, "RandomAccessFile::Map: offset must be nonnegative.");
if (length == 0) {
mapped_memory = MappedMemoryPtr{};
return Status::OK();
}

uint64_t file_size = 0;
ORT_RETURN_IF_ERROR(GetLength(file_size));
const uint64_t requested_end = SafeInt<uint64_t>(offset) + length;
ORT_RETURN_IF(file_size < requested_end, "RandomAccessFile::Map: requested range exceeds file size.");

const long system_page_size = sysconf(_SC_PAGESIZE);
ORT_RETURN_IF_NOT(system_page_size > 0, "sysconf(_SC_PAGESIZE) failed.");
const size_t page_size = narrow<size_t>(system_page_size);
const FileOffsetType offset_to_page = offset % static_cast<FileOffsetType>(page_size);
const size_t mapped_length = SafeInt<size_t>(length) + static_cast<size_t>(offset_to_page);
const FileOffsetType mapped_offset = offset - offset_to_page;
void* const mapped_base =
mmap(nullptr, mapped_length, PROT_READ | PROT_WRITE, MAP_PRIVATE, descriptor_.Get(), mapped_offset);
Comment thread
Copilot marked this conversation as resolved.
Resolved
if (mapped_base == MAP_FAILED) {
return ReportSystemError("mmap", path_);
}

mapped_memory = MappedMemoryPtr{
reinterpret_cast<char*>(mapped_base) + offset_to_page,
MappedMemoryDeleter{mapped_base, mapped_length, [](void* base, size_t mapped_size) noexcept {
UnmapFile(base, mapped_size);
}}};
return Status::OK();
}

common::Status Read(FileOffsetType offset, gsl::span<char> buffer) const override {
Expand Down
Loading