From 988747b9f2ec2bfa55d21234d972afa08147b102 Mon Sep 17 00:00:00 2001 From: Tianlei Wu Date: Sat, 25 Apr 2026 12:53:32 -0700 Subject: [PATCH 1/3] fix(cpuinfo): tolerate missing Linux CPU sysfs lists --- .../external/onnxruntime_external_deps.cmake | 16 +- .../cpuinfo/fix_missing_sysfs_fallback.patch | 83 ++++ onnxruntime/core/platform/posix/env.cc | 12 +- .../common/test_cpuinfo_sysfs_fallback.py | 445 ++++++++++++++++++ 4 files changed, 554 insertions(+), 2 deletions(-) create mode 100644 cmake/patches/cpuinfo/fix_missing_sysfs_fallback.patch create mode 100644 onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py diff --git a/cmake/external/onnxruntime_external_deps.cmake b/cmake/external/onnxruntime_external_deps.cmake index 4afa074a0b254..b7985129859b4 100644 --- a/cmake/external/onnxruntime_external_deps.cmake +++ b/cmake/external/onnxruntime_external_deps.cmake @@ -373,7 +373,21 @@ if (CPUINFO_SUPPORTED) # https://github.com/pytorch/cpuinfo/pull/324 ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/patch_vcpkg_arm64ec_support.patch && # https://github.com/pytorch/cpuinfo/pull/348 - ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/win_arm_fp16_detection_fallback.patch + ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/win_arm_fp16_detection_fallback.patch && + # https://github.com/microsoft/onnxruntime/issues/10038 + ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/fix_missing_sysfs_fallback.patch + FIND_PACKAGE_ARGS NAMES cpuinfo + ) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(STATUS "Applying sysfs fallback patch for cpuinfo on Linux") + onnxruntime_fetchcontent_declare( + pytorch_cpuinfo + URL ${DEP_URL_pytorch_cpuinfo} + URL_HASH SHA1=${DEP_SHA1_pytorch_cpuinfo} + EXCLUDE_FROM_ALL + PATCH_COMMAND + # https://github.com/microsoft/onnxruntime/issues/10038 + ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/fix_missing_sysfs_fallback.patch FIND_PACKAGE_ARGS NAMES cpuinfo ) else() diff --git a/cmake/patches/cpuinfo/fix_missing_sysfs_fallback.patch b/cmake/patches/cpuinfo/fix_missing_sysfs_fallback.patch new file mode 100644 index 0000000000000..005cd458fdd2b --- /dev/null +++ b/cmake/patches/cpuinfo/fix_missing_sysfs_fallback.patch @@ -0,0 +1,83 @@ +diff --git a/src/linux/processors.c b/src/linux/processors.c +index 47bee76..d0c5569 100644 +--- a/src/linux/processors.c ++++ b/src/linux/processors.c +@@ -2,0 +3 @@ ++#include +@@ -291,0 +293,22 @@ ++static uint32_t cpuinfo_linux_get_max_processor_from_sysconf( ++ uint32_t max_processors_count, ++ const char* processor_list_name) { ++ const long nproc = sysconf(_SC_NPROCESSORS_ONLN); ++ if (nproc <= 0) { ++ cpuinfo_log_warning( ++ "failed to query online processors from sysconf(_SC_NPROCESSORS_ONLN) for %s", ++ processor_list_name); ++ return UINT32_MAX; ++ } ++ ++ uint32_t max_processor = (uint32_t)(nproc - 1); ++ if ((uint64_t)nproc > (uint64_t)max_processors_count) { ++ cpuinfo_log_warning( ++ "online processors count %ld exceeds system limit %" PRIu32 ": truncating to the latter", ++ nproc, ++ max_processors_count); ++ max_processor = max_processors_count - 1; ++ } ++ return max_processor; ++} ++ +@@ -301 +324 @@ +- return UINT32_MAX; ++ return cpuinfo_linux_get_max_processor_from_sysconf(max_processors_count, POSSIBLE_CPULIST_FILENAME); +@@ -323 +346 @@ +- return UINT32_MAX; ++ return cpuinfo_linux_get_max_processor_from_sysconf(max_processors_count, PRESENT_CPULIST_FILENAME); +@@ -357,0 +381,31 @@ ++static bool cpuinfo_linux_detect_processors_from_sysconf( ++ uint32_t max_processors_count, ++ uint32_t* processor0_flags, ++ uint32_t processor_struct_size, ++ uint32_t detected_flag, ++ const char* processor_list_name) { ++ const long nproc = sysconf(_SC_NPROCESSORS_ONLN); ++ if (nproc <= 0) { ++ cpuinfo_log_warning( ++ "failed to query online processors from sysconf(_SC_NPROCESSORS_ONLN) for %s", ++ processor_list_name); ++ return false; ++ } ++ ++ uint32_t processors_count = (uint32_t)nproc; ++ if ((uint64_t)nproc > (uint64_t)max_processors_count) { ++ cpuinfo_log_warning( ++ "online processors count %ld exceeds system limit %" PRIu32 ": truncating to the latter", ++ nproc, ++ max_processors_count); ++ processors_count = max_processors_count; ++ } ++ ++ for (uint32_t processor = 0; processor < processors_count; processor++) { ++ *((uint32_t*)((uintptr_t)processor0_flags + processor_struct_size * processor)) |= detected_flag; ++ } ++ cpuinfo_log_warning( ++ "falling back to sysconf(_SC_NPROCESSORS_ONLN) = %ld for %s", nproc, processor_list_name); ++ return true; ++} ++ +@@ -373 +427,6 @@ +- return false; ++ return cpuinfo_linux_detect_processors_from_sysconf( ++ max_processors_count, ++ processor0_flags, ++ processor_struct_size, ++ possible_flag, ++ POSSIBLE_CPULIST_FILENAME); +@@ -392 +451,6 @@ +- return false; ++ return cpuinfo_linux_detect_processors_from_sysconf( ++ max_processors_count, ++ processor0_flags, ++ processor_struct_size, ++ present_flag, ++ PRESENT_CPULIST_FILENAME); diff --git a/onnxruntime/core/platform/posix/env.cc b/onnxruntime/core/platform/posix/env.cc index aeddef0c5188f..28d6332f6282c 100644 --- a/onnxruntime/core/platform/posix/env.cc +++ b/onnxruntime/core/platform/posix/env.cc @@ -618,7 +618,17 @@ class PosixEnv : public Env { PosixEnv() { cpuinfo_available_ = cpuinfo_initialize(); if (!cpuinfo_available_) { - LOGS_DEFAULT(INFO) << "cpuinfo_initialize failed"; + // PosixEnv may be constructed before the logging system is initialized + // (e.g. via a static Env::Default() reference in the Python bindings). + // Using LOGS_DEFAULT here would crash with "Attempt to use DefaultLogger + // but none has been registered". Fall back to stderr when no logger exists. + if (logging::LoggingManager::HasDefaultLogger()) { + LOGS_DEFAULT(WARNING) << "cpuinfo_initialize failed. " + "May cause CPU EP performance degradation due to undetected CPU features."; + } else { + std::cerr << "onnxruntime warning: cpuinfo_initialize failed. " + "May cause CPU EP performance degradation due to undetected CPU features.\n"; + } } } bool cpuinfo_available_{false}; diff --git a/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py b/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py new file mode 100644 index 0000000000000..39a744eee567d --- /dev/null +++ b/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python3 +""" +Simulation test for the cpuinfo sysfs fallback fix. + +This test verifies two fixes for https://github.com/microsoft/onnxruntime/issues/10038: + +1. Safe logging in env.cc - PosixEnv constructor no longer crashes when the + logging system is not yet initialized and cpuinfo_initialize() fails. + +2. cpuinfo sysfs fallback - The patched cpuinfo library falls back to + sysconf(_SC_NPROCESSORS_ONLN) for both processor counts and per-CPU + present/possible flags when /sys/devices/system/cpu/{possible,present} + files are missing. + +Testing approach: +- Test 1: Compile a small C++ program that calls the safe logging pattern + without a registered logger. Verify it doesn't crash. +- Test 2: Compile a small C program that validates the sysconf fallback + arithmetic and verifies that the fallback marks each online CPU with both + PRESENT and POSSIBLE flags. This catches the incomplete count-only fallback. +- Test 3: Use an LD_PRELOAD shim (like the lambda-arm64-onnx workaround) + to simulate missing sysfs files and verify ORT loads without crash. + +Note: Tests 2 and 3 require a build of ORT with the patches applied. +Test 1 can run standalone. +""" + +import os +import subprocess +import sys +import tempfile +import textwrap + + +def get_ort_root(): + """Get the ORT repository root.""" + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def test_safe_logging_pattern(): + """ + Test 1: Verify the safe logging pattern doesn't crash when no logger exists. + + This simulates the fix in env.cc where we check HasDefaultLogger() before + calling LOGS_DEFAULT(). We compile a minimal C++ program that: + - Does NOT register a default logger + - Calls the safe logging pattern + - Verifies it writes to stderr instead of crashing + """ + print("=" * 60) + print("Test 1: Safe logging pattern (no default logger)") + print("=" * 60) + + source = textwrap.dedent(r""" + #include + #include + + // Minimal simulation of ORT's logging check pattern + namespace logging { + class LoggingManager { + public: + // Simulate: no default logger registered + static bool HasDefaultLogger() { return false; } + }; + } // namespace logging + + void LogEarlyWarning(std::string_view message) { + if (logging::LoggingManager::HasDefaultLogger()) { + // Would call LOGS_DEFAULT(WARNING) here - but logger doesn't exist + // This path should NOT be taken + std::cerr << "BUG: should not reach here\n"; + return; + } + // Safe fallback to stderr + std::cerr << "onnxruntime warning: " << message << "\n"; + } + + int main() { + // This simulates what PosixEnv() does when cpuinfo_initialize() fails + bool cpuinfo_available = false; // Simulating failure + if (!cpuinfo_available) { + LogEarlyWarning("cpuinfo_initialize failed. " + "May cause CPU EP performance degradation due to undetected CPU features."); + } + std::cout << "PASS: Safe logging pattern works without crash\n"; + return 0; + } + """) + + with tempfile.NamedTemporaryFile(suffix=".cc", mode="w", delete=False) as f: + f.write(source) + src_path = f.name + + try: + exe_path = src_path.replace(".cc", "") + result = subprocess.run( + ["g++", "-std=c++17", "-o", exe_path, src_path], check=False, capture_output=True, text=True + ) + if result.returncode != 0: + print(f"FAIL: Compilation failed: {result.stderr}") + return False + + result = subprocess.run([exe_path], check=False, capture_output=True, text=True, timeout=10) + if result.returncode != 0: + print(f"FAIL: Program crashed with exit code {result.returncode}") + print(f"stderr: {result.stderr}") + return False + + if "PASS" in result.stdout: + print(result.stdout.strip()) + print(f"stderr output (expected): {result.stderr.strip()}") + return True + print(f"FAIL: Unexpected output: {result.stdout}") + return False + finally: + os.unlink(src_path) + if os.path.exists(src_path.replace(".cc", "")): + os.unlink(src_path.replace(".cc", "")) + + +def test_sysconf_fallback(): + """ + Test 2: Verify sysconf(_SC_NPROCESSORS_ONLN) works as a complete fallback. + + This doesn't test the actual cpuinfo patch (that requires building cpuinfo) + but verifies the fallback mechanism produces correct counts and marks + present/possible flags for each online CPU. + """ + print() + print("=" * 60) + print("Test 2: sysconf(_SC_NPROCESSORS_ONLN) count and flag fallback validation") + print("=" * 60) + + source = textwrap.dedent(r""" + #include + #include + #include + + #define CPUINFO_LINUX_FLAG_PRESENT 0x1 + #define CPUINFO_LINUX_FLAG_POSSIBLE 0x2 + + int main() { + long nproc = sysconf(_SC_NPROCESSORS_ONLN); + if (nproc <= 0) { + printf("FAIL: sysconf(_SC_NPROCESSORS_ONLN) returned %ld\n", nproc); + return 1; + } + // Simulate what the patched cpuinfo max-count helpers return: + // max_processor = nproc - 1 (0-indexed). Then arm_linux_init does: + // 1 + max_processor = nproc. + unsigned int max_processor = (unsigned int)(nproc - 1); + unsigned int arm_linux_processors_count = 1 + max_processor; + + uint32_t processor_flags[1024] = {0}; + unsigned int processors_count = arm_linux_processors_count; + if (processors_count > 1024) { + processors_count = 1024; + } + + // Simulate cpuinfo_linux_detect_possible_processors() and + // cpuinfo_linux_detect_present_processors() fallback helpers. + for (unsigned int processor = 0; processor < processors_count; ++processor) { + processor_flags[processor] |= CPUINFO_LINUX_FLAG_PRESENT; + processor_flags[processor] |= CPUINFO_LINUX_FLAG_POSSIBLE; + } + + unsigned int valid_processors = 0; + const uint32_t valid_processor_mask = CPUINFO_LINUX_FLAG_PRESENT | CPUINFO_LINUX_FLAG_POSSIBLE; + for (unsigned int processor = 0; processor < processors_count; ++processor) { + if ((processor_flags[processor] & valid_processor_mask) == valid_processor_mask) { + ++valid_processors; + } + } + + printf("sysconf(_SC_NPROCESSORS_ONLN) = %ld\n", nproc); + printf("Simulated max_processor = %u\n", max_processor); + printf("Simulated arm_linux_processors_count = %u\n", arm_linux_processors_count); + printf("Simulated valid_processors = %u\n", valid_processors); + + if (arm_linux_processors_count == (unsigned int)nproc && valid_processors == processors_count) { + printf("PASS: Fallback produces correct processor count and flags\n"); + return 0; + } + printf("FAIL: Processor count or flags mismatch\n"); + return 1; + } + """) + + with tempfile.NamedTemporaryFile(suffix=".c", mode="w", delete=False) as f: + f.write(source) + src_path = f.name + + try: + exe_path = src_path.replace(".c", "") + result = subprocess.run(["gcc", "-o", exe_path, src_path], check=False, capture_output=True, text=True) + if result.returncode != 0: + print(f"FAIL: Compilation failed: {result.stderr}") + return False + + result = subprocess.run([exe_path], check=False, capture_output=True, text=True, timeout=10) + print(result.stdout.strip()) + if result.returncode != 0: + print(f"FAIL: exit code {result.returncode}") + return False + return "PASS" in result.stdout + finally: + os.unlink(src_path) + if os.path.exists(src_path.replace(".c", "")): + os.unlink(src_path.replace(".c", "")) + + +def test_sysfs_hide_with_ld_preload(): + """ + Test 3: Verify LD_PRELOAD shim can hide sysfs files. + + This compiles a small shim that intercepts open/fopen to return ENOENT + for /sys/devices/system/cpu/{possible,present}, then runs a test program + that tries to read those files. + """ + print() + print("=" * 60) + print("Test 3: LD_PRELOAD sysfs-hiding shim") + print("=" * 60) + + shim_source = textwrap.dedent(r""" + #define _GNU_SOURCE + #include + #include + #include + #include + + static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; + static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; + + static int is_blocked(const char *path) { + return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); + } + + FILE *fopen(const char *restrict path, const char *restrict mode) { + static FILE *(*real_fopen)(const char *, const char *) = NULL; + if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); + + if (is_blocked(path)) { + errno = ENOENT; + return NULL; + } + return real_fopen(path, mode); + } + """) + + test_source = textwrap.dedent(r""" + #include + #include + #include + + int main() { + FILE *f; + int pass = 1; + + f = fopen("/sys/devices/system/cpu/possible", "r"); + if (f != NULL) { + printf("FAIL: /sys/devices/system/cpu/possible should be blocked\n"); + fclose(f); + pass = 0; + } else { + printf("OK: /sys/devices/system/cpu/possible blocked (errno=%d: %s)\n", + errno, strerror(errno)); + } + + f = fopen("/sys/devices/system/cpu/present", "r"); + if (f != NULL) { + printf("FAIL: /sys/devices/system/cpu/present should be blocked\n"); + fclose(f); + pass = 0; + } else { + printf("OK: /sys/devices/system/cpu/present blocked (errno=%d: %s)\n", + errno, strerror(errno)); + } + + // Verify other files still work + f = fopen("/proc/cpuinfo", "r"); + if (f == NULL) { + printf("WARN: /proc/cpuinfo not accessible (may be OK in some envs)\n"); + } else { + printf("OK: /proc/cpuinfo still accessible\n"); + fclose(f); + } + + if (pass) { + printf("PASS: LD_PRELOAD sysfs-hiding shim works correctly\n"); + } + return pass ? 0 : 1; + } + """) + + with tempfile.TemporaryDirectory() as tmpdir: + shim_path = os.path.join(tmpdir, "hide_sysfs.c") + shim_so = os.path.join(tmpdir, "hide_sysfs.so") + test_path = os.path.join(tmpdir, "test_sysfs.c") + test_exe = os.path.join(tmpdir, "test_sysfs") + + with open(shim_path, "w") as f: + f.write(shim_source) + with open(test_path, "w") as f: + f.write(test_source) + + # Compile shim + result = subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", shim_so, shim_path, "-ldl"], check=False, capture_output=True, text=True + ) + if result.returncode != 0: + print(f"FAIL: Shim compilation failed: {result.stderr}") + return False + + # Compile test + result = subprocess.run(["gcc", "-o", test_exe, test_path], check=False, capture_output=True, text=True) + if result.returncode != 0: + print(f"FAIL: Test compilation failed: {result.stderr}") + return False + + # Run with LD_PRELOAD + env = os.environ.copy() + env["LD_PRELOAD"] = shim_so + result = subprocess.run([test_exe], check=False, capture_output=True, text=True, timeout=10, env=env) + print(result.stdout.strip()) + if result.returncode != 0: + print(f"FAIL: exit code {result.returncode}") + return False + return "PASS" in result.stdout + + +def test_ort_import_with_hidden_sysfs(): + """ + Test 4: Integration test - import onnxruntime with hidden sysfs files. + + This uses the LD_PRELOAD shim to hide /sys/devices/system/cpu/{possible,present} + and then imports onnxruntime. This is the actual end-to-end test that + verifies both fixes work together. + + NOTE: This requires onnxruntime to be built with the patches applied. + """ + print() + print("=" * 60) + print("Test 4: Import onnxruntime with hidden sysfs (integration)") + print("=" * 60) + + # Check if onnxruntime is importable + result = subprocess.run( + [sys.executable, "-c", "import onnxruntime"], check=False, capture_output=True, text=True, timeout=30 + ) + if result.returncode != 0: + print("SKIP: onnxruntime not installed/importable") + return None + + shim_source = textwrap.dedent(r""" + #define _GNU_SOURCE + #include + #include + #include + #include + + static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; + static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; + + static int is_blocked(const char *path) { + return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); + } + + FILE *fopen(const char *restrict path, const char *restrict mode) { + static FILE *(*real_fopen)(const char *, const char *) = NULL; + if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); + if (is_blocked(path)) { errno = ENOENT; return NULL; } + return real_fopen(path, mode); + } + """) + + with tempfile.TemporaryDirectory() as tmpdir: + shim_path = os.path.join(tmpdir, "hide_sysfs.c") + shim_so = os.path.join(tmpdir, "hide_sysfs.so") + + with open(shim_path, "w") as f: + f.write(shim_source) + + result = subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", shim_so, shim_path, "-ldl"], check=False, capture_output=True, text=True + ) + if result.returncode != 0: + print(f"FAIL: Shim compilation failed: {result.stderr}") + return False + + env = os.environ.copy() + env["LD_PRELOAD"] = shim_so + + # Try importing onnxruntime with hidden sysfs + result = subprocess.run( + [ + sys.executable, + "-c", + "import onnxruntime; print('PASS: onnxruntime imported successfully'); " + "print(f'Version: {onnxruntime.__version__}'); " + "print(f'Providers: {onnxruntime.get_available_providers()}')", + ], + check=False, + capture_output=True, + text=True, + timeout=60, + env=env, + ) + print(f"stdout: {result.stdout.strip()}") + if result.stderr: + print(f"stderr: {result.stderr.strip()}") + if result.returncode != 0: + print(f"FAIL: exit code {result.returncode}") + return False + return "PASS" in result.stdout + + +def main(): + results = {} + + results["safe_logging"] = test_safe_logging_pattern() + results["sysconf_fallback"] = test_sysconf_fallback() + results["ld_preload_shim"] = test_sysfs_hide_with_ld_preload() + results["ort_import"] = test_ort_import_with_hidden_sysfs() + + print() + print("=" * 60) + print("Summary") + print("=" * 60) + all_pass = True + for name, result in results.items(): + if result is None: + status = "SKIP" + elif result: + status = "PASS" + else: + status = "FAIL" + all_pass = False + print(f" {name}: {status}") + + return 0 if all_pass else 1 + + +if __name__ == "__main__": + sys.exit(main()) From 183963c425adbdf71775edc18a4cb9071bfa1e32 Mon Sep 17 00:00:00 2001 From: Tianlei Wu Date: Tue, 28 Apr 2026 14:41:31 -0700 Subject: [PATCH 2/3] Address review feedback: convert test to unittest, add platform guards, fix docstring - Convert test_cpuinfo_sysfs_fallback.py from standalone functions to proper unittest.TestCase so pytest/unittest discovery works correctly - Add platform guards (sys.platform == 'linux') and tool detection (shutil.which) with unittest.SkipTest for non-Linux or missing compilers - Remove unused get_ort_root() function - Fix docstring: 'intercepts open/fopen' -> 'intercepts fopen' to match impl - Fix CodeQL implicit string concatenation warning by extracting the -c script to a named variable - Remove fix_missing_sysfs_fallback.patch from Windows ARM64/ARM64EC block since it only modifies Linux-specific sources (src/linux/processors.c); keep it in the Linux-only elseif block --- .../external/onnxruntime_external_deps.cmake | 4 +- .../common/test_cpuinfo_sysfs_fallback.py | 713 ++++++++---------- 2 files changed, 331 insertions(+), 386 deletions(-) diff --git a/cmake/external/onnxruntime_external_deps.cmake b/cmake/external/onnxruntime_external_deps.cmake index b7985129859b4..be0abc980bda6 100644 --- a/cmake/external/onnxruntime_external_deps.cmake +++ b/cmake/external/onnxruntime_external_deps.cmake @@ -373,9 +373,7 @@ if (CPUINFO_SUPPORTED) # https://github.com/pytorch/cpuinfo/pull/324 ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/patch_vcpkg_arm64ec_support.patch && # https://github.com/pytorch/cpuinfo/pull/348 - ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/win_arm_fp16_detection_fallback.patch && - # https://github.com/microsoft/onnxruntime/issues/10038 - ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/fix_missing_sysfs_fallback.patch + ${Patch_EXECUTABLE} -p1 < ${PROJECT_SOURCE_DIR}/patches/cpuinfo/win_arm_fp16_detection_fallback.patch FIND_PACKAGE_ARGS NAMES cpuinfo ) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") diff --git a/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py b/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py index 39a744eee567d..2ab1271f54595 100644 --- a/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py +++ b/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py @@ -8,16 +8,16 @@ logging system is not yet initialized and cpuinfo_initialize() fails. 2. cpuinfo sysfs fallback - The patched cpuinfo library falls back to - sysconf(_SC_NPROCESSORS_ONLN) for both processor counts and per-CPU - present/possible flags when /sys/devices/system/cpu/{possible,present} - files are missing. + sysconf(_SC_NPROCESSORS_ONLN) for both processor counts and per-CPU + present/possible flags when /sys/devices/system/cpu/{possible,present} + files are missing. Testing approach: - Test 1: Compile a small C++ program that calls the safe logging pattern without a registered logger. Verify it doesn't crash. - Test 2: Compile a small C program that validates the sysconf fallback - arithmetic and verifies that the fallback marks each online CPU with both - PRESENT and POSSIBLE flags. This catches the incomplete count-only fallback. + arithmetic and verifies that the fallback marks each online CPU with both + PRESENT and POSSIBLE flags. This catches the incomplete count-only fallback. - Test 3: Use an LD_PRELOAD shim (like the lambda-arm64-onnx workaround) to simulate missing sysfs files and verify ORT loads without crash. @@ -26,420 +26,367 @@ """ import os +import shutil import subprocess import sys import tempfile import textwrap +import unittest -def get_ort_root(): - """Get the ORT repository root.""" - return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -def test_safe_logging_pattern(): - """ - Test 1: Verify the safe logging pattern doesn't crash when no logger exists. - - This simulates the fix in env.cc where we check HasDefaultLogger() before - calling LOGS_DEFAULT(). We compile a minimal C++ program that: - - Does NOT register a default logger - - Calls the safe logging pattern - - Verifies it writes to stderr instead of crashing - """ - print("=" * 60) - print("Test 1: Safe logging pattern (no default logger)") - print("=" * 60) - - source = textwrap.dedent(r""" - #include - #include - - // Minimal simulation of ORT's logging check pattern - namespace logging { - class LoggingManager { - public: - // Simulate: no default logger registered - static bool HasDefaultLogger() { return false; } - }; - } // namespace logging - - void LogEarlyWarning(std::string_view message) { - if (logging::LoggingManager::HasDefaultLogger()) { - // Would call LOGS_DEFAULT(WARNING) here - but logger doesn't exist - // This path should NOT be taken - std::cerr << "BUG: should not reach here\n"; - return; - } - // Safe fallback to stderr - std::cerr << "onnxruntime warning: " << message << "\n"; - } - - int main() { - // This simulates what PosixEnv() does when cpuinfo_initialize() fails - bool cpuinfo_available = false; // Simulating failure - if (!cpuinfo_available) { - LogEarlyWarning("cpuinfo_initialize failed. " - "May cause CPU EP performance degradation due to undetected CPU features."); - } - std::cout << "PASS: Safe logging pattern works without crash\n"; - return 0; - } - """) +def _require_linux(): + if sys.platform != "linux": + raise unittest.SkipTest("Test requires Linux") - with tempfile.NamedTemporaryFile(suffix=".cc", mode="w", delete=False) as f: - f.write(source) - src_path = f.name - try: - exe_path = src_path.replace(".cc", "") - result = subprocess.run( - ["g++", "-std=c++17", "-o", exe_path, src_path], check=False, capture_output=True, text=True - ) - if result.returncode != 0: - print(f"FAIL: Compilation failed: {result.stderr}") - return False +def _require_gcc(): + if not shutil.which("gcc"): + raise unittest.SkipTest("gcc not found") - result = subprocess.run([exe_path], check=False, capture_output=True, text=True, timeout=10) - if result.returncode != 0: - print(f"FAIL: Program crashed with exit code {result.returncode}") - print(f"stderr: {result.stderr}") - return False - - if "PASS" in result.stdout: - print(result.stdout.strip()) - print(f"stderr output (expected): {result.stderr.strip()}") - return True - print(f"FAIL: Unexpected output: {result.stdout}") - return False - finally: - os.unlink(src_path) - if os.path.exists(src_path.replace(".cc", "")): - os.unlink(src_path.replace(".cc", "")) - - -def test_sysconf_fallback(): - """ - Test 2: Verify sysconf(_SC_NPROCESSORS_ONLN) works as a complete fallback. - - This doesn't test the actual cpuinfo patch (that requires building cpuinfo) - but verifies the fallback mechanism produces correct counts and marks - present/possible flags for each online CPU. - """ - print() - print("=" * 60) - print("Test 2: sysconf(_SC_NPROCESSORS_ONLN) count and flag fallback validation") - print("=" * 60) - - source = textwrap.dedent(r""" - #include - #include - #include - - #define CPUINFO_LINUX_FLAG_PRESENT 0x1 - #define CPUINFO_LINUX_FLAG_POSSIBLE 0x2 - - int main() { - long nproc = sysconf(_SC_NPROCESSORS_ONLN); - if (nproc <= 0) { - printf("FAIL: sysconf(_SC_NPROCESSORS_ONLN) returned %ld\n", nproc); - return 1; - } - // Simulate what the patched cpuinfo max-count helpers return: - // max_processor = nproc - 1 (0-indexed). Then arm_linux_init does: - // 1 + max_processor = nproc. - unsigned int max_processor = (unsigned int)(nproc - 1); - unsigned int arm_linux_processors_count = 1 + max_processor; - - uint32_t processor_flags[1024] = {0}; - unsigned int processors_count = arm_linux_processors_count; - if (processors_count > 1024) { - processors_count = 1024; - } - // Simulate cpuinfo_linux_detect_possible_processors() and - // cpuinfo_linux_detect_present_processors() fallback helpers. - for (unsigned int processor = 0; processor < processors_count; ++processor) { - processor_flags[processor] |= CPUINFO_LINUX_FLAG_PRESENT; - processor_flags[processor] |= CPUINFO_LINUX_FLAG_POSSIBLE; - } +def _require_gpp(): + if not shutil.which("g++"): + raise unittest.SkipTest("g++ not found") + + +class TestCpuinfoSysfsFallback(unittest.TestCase): + def test_safe_logging_pattern(self): + """Verify the safe logging pattern doesn't crash when no logger exists. + + This simulates the fix in env.cc where we check HasDefaultLogger() before + calling LOGS_DEFAULT(). We compile a minimal C++ program that: + - Does NOT register a default logger + - Calls the safe logging pattern + - Verifies it writes to stderr instead of crashing + """ + _require_linux() + _require_gpp() - unsigned int valid_processors = 0; - const uint32_t valid_processor_mask = CPUINFO_LINUX_FLAG_PRESENT | CPUINFO_LINUX_FLAG_POSSIBLE; - for (unsigned int processor = 0; processor < processors_count; ++processor) { - if ((processor_flags[processor] & valid_processor_mask) == valid_processor_mask) { - ++valid_processors; + source = textwrap.dedent(r""" + #include + #include + + // Minimal simulation of ORT's logging check pattern + namespace logging { + class LoggingManager { + public: + // Simulate: no default logger registered + static bool HasDefaultLogger() { return false; } + }; + } // namespace logging + + void LogEarlyWarning(std::string_view message) { + if (logging::LoggingManager::HasDefaultLogger()) { + // Would call LOGS_DEFAULT(WARNING) here - but logger doesn't exist + // This path should NOT be taken + std::cerr << "BUG: should not reach here\n"; + return; } + // Safe fallback to stderr + std::cerr << "onnxruntime warning: " << message << "\n"; } - printf("sysconf(_SC_NPROCESSORS_ONLN) = %ld\n", nproc); - printf("Simulated max_processor = %u\n", max_processor); - printf("Simulated arm_linux_processors_count = %u\n", arm_linux_processors_count); - printf("Simulated valid_processors = %u\n", valid_processors); - - if (arm_linux_processors_count == (unsigned int)nproc && valid_processors == processors_count) { - printf("PASS: Fallback produces correct processor count and flags\n"); + int main() { + // This simulates what PosixEnv() does when cpuinfo_initialize() fails + bool cpuinfo_available = false; // Simulating failure + if (!cpuinfo_available) { + LogEarlyWarning("cpuinfo_initialize failed. " + "May cause CPU EP performance degradation due to undetected CPU features."); + } + std::cout << "PASS: Safe logging pattern works without crash\n"; return 0; } - printf("FAIL: Processor count or flags mismatch\n"); - return 1; - } - """) - - with tempfile.NamedTemporaryFile(suffix=".c", mode="w", delete=False) as f: - f.write(source) - src_path = f.name - - try: - exe_path = src_path.replace(".c", "") - result = subprocess.run(["gcc", "-o", exe_path, src_path], check=False, capture_output=True, text=True) - if result.returncode != 0: - print(f"FAIL: Compilation failed: {result.stderr}") - return False + """) + + with tempfile.NamedTemporaryFile(suffix=".cc", mode="w", delete=False) as f: + f.write(source) + src_path = f.name + + try: + exe_path = src_path.replace(".cc", "") + result = subprocess.run( + ["g++", "-std=c++17", "-o", exe_path, src_path], check=False, capture_output=True, text=True + ) + self.assertEqual(result.returncode, 0, f"Compilation failed: {result.stderr}") + + result = subprocess.run([exe_path], check=False, capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 0, f"Program crashed with exit code {result.returncode}: {result.stderr}") + self.assertIn("PASS", result.stdout) + finally: + os.unlink(src_path) + if os.path.exists(src_path.replace(".cc", "")): + os.unlink(src_path.replace(".cc", "")) + + def test_sysconf_fallback(self): + """Verify sysconf(_SC_NPROCESSORS_ONLN) works as a complete fallback. + + This doesn't test the actual cpuinfo patch (that requires building cpuinfo) + but verifies the fallback mechanism produces correct counts and marks + present/possible flags for each online CPU. + """ + _require_linux() + _require_gcc() + + source = textwrap.dedent(r""" + #include + #include + #include + + #define CPUINFO_LINUX_FLAG_PRESENT 0x1 + #define CPUINFO_LINUX_FLAG_POSSIBLE 0x2 + + int main() { + long nproc = sysconf(_SC_NPROCESSORS_ONLN); + if (nproc <= 0) { + printf("FAIL: sysconf(_SC_NPROCESSORS_ONLN) returned %ld\n", nproc); + return 1; + } + // Simulate what the patched cpuinfo max-count helpers return: + // max_processor = nproc - 1 (0-indexed). Then arm_linux_init does: + // 1 + max_processor = nproc. + unsigned int max_processor = (unsigned int)(nproc - 1); + unsigned int arm_linux_processors_count = 1 + max_processor; + + uint32_t processor_flags[1024] = {0}; + unsigned int processors_count = arm_linux_processors_count; + if (processors_count > 1024) { + processors_count = 1024; + } - result = subprocess.run([exe_path], check=False, capture_output=True, text=True, timeout=10) - print(result.stdout.strip()) - if result.returncode != 0: - print(f"FAIL: exit code {result.returncode}") - return False - return "PASS" in result.stdout - finally: - os.unlink(src_path) - if os.path.exists(src_path.replace(".c", "")): - os.unlink(src_path.replace(".c", "")) - - -def test_sysfs_hide_with_ld_preload(): - """ - Test 3: Verify LD_PRELOAD shim can hide sysfs files. - - This compiles a small shim that intercepts open/fopen to return ENOENT - for /sys/devices/system/cpu/{possible,present}, then runs a test program - that tries to read those files. - """ - print() - print("=" * 60) - print("Test 3: LD_PRELOAD sysfs-hiding shim") - print("=" * 60) - - shim_source = textwrap.dedent(r""" - #define _GNU_SOURCE - #include - #include - #include - #include - - static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; - static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; - - static int is_blocked(const char *path) { - return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); - } - - FILE *fopen(const char *restrict path, const char *restrict mode) { - static FILE *(*real_fopen)(const char *, const char *) = NULL; - if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); - - if (is_blocked(path)) { - errno = ENOENT; - return NULL; + // Simulate cpuinfo_linux_detect_possible_processors() and + // cpuinfo_linux_detect_present_processors() fallback helpers. + for (unsigned int processor = 0; processor < processors_count; ++processor) { + processor_flags[processor] |= CPUINFO_LINUX_FLAG_PRESENT; + processor_flags[processor] |= CPUINFO_LINUX_FLAG_POSSIBLE; + } + + unsigned int valid_processors = 0; + const uint32_t valid_processor_mask = CPUINFO_LINUX_FLAG_PRESENT | CPUINFO_LINUX_FLAG_POSSIBLE; + for (unsigned int processor = 0; processor < processors_count; ++processor) { + if ((processor_flags[processor] & valid_processor_mask) == valid_processor_mask) { + ++valid_processors; + } + } + + printf("sysconf(_SC_NPROCESSORS_ONLN) = %ld\n", nproc); + printf("Simulated max_processor = %u\n", max_processor); + printf("Simulated arm_linux_processors_count = %u\n", arm_linux_processors_count); + printf("Simulated valid_processors = %u\n", valid_processors); + + if (arm_linux_processors_count == (unsigned int)nproc && valid_processors == processors_count) { + printf("PASS: Fallback produces correct processor count and flags\n"); + return 0; + } + printf("FAIL: Processor count or flags mismatch\n"); + return 1; } - return real_fopen(path, mode); - } - """) - - test_source = textwrap.dedent(r""" - #include - #include - #include - - int main() { - FILE *f; - int pass = 1; - - f = fopen("/sys/devices/system/cpu/possible", "r"); - if (f != NULL) { - printf("FAIL: /sys/devices/system/cpu/possible should be blocked\n"); - fclose(f); - pass = 0; - } else { - printf("OK: /sys/devices/system/cpu/possible blocked (errno=%d: %s)\n", - errno, strerror(errno)); + """) + + with tempfile.NamedTemporaryFile(suffix=".c", mode="w", delete=False) as f: + f.write(source) + src_path = f.name + + try: + exe_path = src_path.replace(".c", "") + result = subprocess.run(["gcc", "-o", exe_path, src_path], check=False, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, f"Compilation failed: {result.stderr}") + + result = subprocess.run([exe_path], check=False, capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 0, f"exit code {result.returncode}: {result.stdout}") + self.assertIn("PASS", result.stdout) + finally: + os.unlink(src_path) + if os.path.exists(src_path.replace(".c", "")): + os.unlink(src_path.replace(".c", "")) + + def test_sysfs_hide_with_ld_preload(self): + """Verify LD_PRELOAD shim can hide sysfs files. + + This compiles a small shim that intercepts fopen to return ENOENT + for /sys/devices/system/cpu/{possible,present}, then runs a test program + that tries to read those files. + """ + _require_linux() + _require_gcc() + + shim_source = textwrap.dedent(r""" + #define _GNU_SOURCE + #include + #include + #include + #include + + static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; + static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; + + static int is_blocked(const char *path) { + return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); } - f = fopen("/sys/devices/system/cpu/present", "r"); - if (f != NULL) { - printf("FAIL: /sys/devices/system/cpu/present should be blocked\n"); - fclose(f); - pass = 0; - } else { - printf("OK: /sys/devices/system/cpu/present blocked (errno=%d: %s)\n", - errno, strerror(errno)); - } + FILE *fopen(const char *restrict path, const char *restrict mode) { + static FILE *(*real_fopen)(const char *, const char *) = NULL; + if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); - // Verify other files still work - f = fopen("/proc/cpuinfo", "r"); - if (f == NULL) { - printf("WARN: /proc/cpuinfo not accessible (may be OK in some envs)\n"); - } else { - printf("OK: /proc/cpuinfo still accessible\n"); - fclose(f); + if (is_blocked(path)) { + errno = ENOENT; + return NULL; + } + return real_fopen(path, mode); } + """) + + test_source = textwrap.dedent(r""" + #include + #include + #include + + int main() { + FILE *f; + int pass = 1; + + f = fopen("/sys/devices/system/cpu/possible", "r"); + if (f != NULL) { + printf("FAIL: /sys/devices/system/cpu/possible should be blocked\n"); + fclose(f); + pass = 0; + } else { + printf("OK: /sys/devices/system/cpu/possible blocked (errno=%d: %s)\n", + errno, strerror(errno)); + } - if (pass) { - printf("PASS: LD_PRELOAD sysfs-hiding shim works correctly\n"); + f = fopen("/sys/devices/system/cpu/present", "r"); + if (f != NULL) { + printf("FAIL: /sys/devices/system/cpu/present should be blocked\n"); + fclose(f); + pass = 0; + } else { + printf("OK: /sys/devices/system/cpu/present blocked (errno=%d: %s)\n", + errno, strerror(errno)); + } + + // Verify other files still work + f = fopen("/proc/cpuinfo", "r"); + if (f == NULL) { + printf("WARN: /proc/cpuinfo not accessible (may be OK in some envs)\n"); + } else { + printf("OK: /proc/cpuinfo still accessible\n"); + fclose(f); + } + + if (pass) { + printf("PASS: LD_PRELOAD sysfs-hiding shim works correctly\n"); + } + return pass ? 0 : 1; } - return pass ? 0 : 1; - } - """) - - with tempfile.TemporaryDirectory() as tmpdir: - shim_path = os.path.join(tmpdir, "hide_sysfs.c") - shim_so = os.path.join(tmpdir, "hide_sysfs.so") - test_path = os.path.join(tmpdir, "test_sysfs.c") - test_exe = os.path.join(tmpdir, "test_sysfs") - - with open(shim_path, "w") as f: - f.write(shim_source) - with open(test_path, "w") as f: - f.write(test_source) - - # Compile shim + """) + + with tempfile.TemporaryDirectory() as tmpdir: + shim_path = os.path.join(tmpdir, "hide_sysfs.c") + shim_so = os.path.join(tmpdir, "hide_sysfs.so") + test_path = os.path.join(tmpdir, "test_sysfs.c") + test_exe = os.path.join(tmpdir, "test_sysfs") + + with open(shim_path, "w") as f: + f.write(shim_source) + with open(test_path, "w") as f: + f.write(test_source) + + # Compile shim + result = subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", shim_so, shim_path, "-ldl"], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, f"Shim compilation failed: {result.stderr}") + + # Compile test + result = subprocess.run(["gcc", "-o", test_exe, test_path], check=False, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, f"Test compilation failed: {result.stderr}") + + # Run with LD_PRELOAD + env = os.environ.copy() + env["LD_PRELOAD"] = shim_so + result = subprocess.run([test_exe], check=False, capture_output=True, text=True, timeout=10, env=env) + self.assertEqual(result.returncode, 0, f"exit code {result.returncode}: {result.stdout}") + self.assertIn("PASS", result.stdout) + + def test_ort_import_with_hidden_sysfs(self): + """Integration test - import onnxruntime with hidden sysfs files. + + This uses the LD_PRELOAD shim to hide /sys/devices/system/cpu/{possible,present} + and then imports onnxruntime. This is the actual end-to-end test that + verifies both fixes work together. + + NOTE: This requires onnxruntime to be built with the patches applied. + """ + _require_linux() + _require_gcc() + + # Check if onnxruntime is importable result = subprocess.run( - ["gcc", "-shared", "-fPIC", "-o", shim_so, shim_path, "-ldl"], check=False, capture_output=True, text=True + [sys.executable, "-c", "import onnxruntime"], check=False, capture_output=True, text=True, timeout=30 ) if result.returncode != 0: - print(f"FAIL: Shim compilation failed: {result.stderr}") - return False + self.skipTest("onnxruntime not installed/importable") - # Compile test - result = subprocess.run(["gcc", "-o", test_exe, test_path], check=False, capture_output=True, text=True) - if result.returncode != 0: - print(f"FAIL: Test compilation failed: {result.stderr}") - return False - - # Run with LD_PRELOAD - env = os.environ.copy() - env["LD_PRELOAD"] = shim_so - result = subprocess.run([test_exe], check=False, capture_output=True, text=True, timeout=10, env=env) - print(result.stdout.strip()) - if result.returncode != 0: - print(f"FAIL: exit code {result.returncode}") - return False - return "PASS" in result.stdout - - -def test_ort_import_with_hidden_sysfs(): - """ - Test 4: Integration test - import onnxruntime with hidden sysfs files. - - This uses the LD_PRELOAD shim to hide /sys/devices/system/cpu/{possible,present} - and then imports onnxruntime. This is the actual end-to-end test that - verifies both fixes work together. - - NOTE: This requires onnxruntime to be built with the patches applied. - """ - print() - print("=" * 60) - print("Test 4: Import onnxruntime with hidden sysfs (integration)") - print("=" * 60) - - # Check if onnxruntime is importable - result = subprocess.run( - [sys.executable, "-c", "import onnxruntime"], check=False, capture_output=True, text=True, timeout=30 - ) - if result.returncode != 0: - print("SKIP: onnxruntime not installed/importable") - return None - - shim_source = textwrap.dedent(r""" - #define _GNU_SOURCE - #include - #include - #include - #include - - static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; - static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; - - static int is_blocked(const char *path) { - return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); - } - - FILE *fopen(const char *restrict path, const char *restrict mode) { - static FILE *(*real_fopen)(const char *, const char *) = NULL; - if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); - if (is_blocked(path)) { errno = ENOENT; return NULL; } - return real_fopen(path, mode); - } - """) - - with tempfile.TemporaryDirectory() as tmpdir: - shim_path = os.path.join(tmpdir, "hide_sysfs.c") - shim_so = os.path.join(tmpdir, "hide_sysfs.so") - - with open(shim_path, "w") as f: - f.write(shim_source) + shim_source = textwrap.dedent(r""" + #define _GNU_SOURCE + #include + #include + #include + #include - result = subprocess.run( - ["gcc", "-shared", "-fPIC", "-o", shim_so, shim_path, "-ldl"], check=False, capture_output=True, text=True - ) - if result.returncode != 0: - print(f"FAIL: Shim compilation failed: {result.stderr}") - return False + static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; + static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; - env = os.environ.copy() - env["LD_PRELOAD"] = shim_so + static int is_blocked(const char *path) { + return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); + } - # Try importing onnxruntime with hidden sysfs - result = subprocess.run( - [ - sys.executable, - "-c", - "import onnxruntime; print('PASS: onnxruntime imported successfully'); " - "print(f'Version: {onnxruntime.__version__}'); " - "print(f'Providers: {onnxruntime.get_available_providers()}')", - ], - check=False, - capture_output=True, - text=True, - timeout=60, - env=env, - ) - print(f"stdout: {result.stdout.strip()}") - if result.stderr: - print(f"stderr: {result.stderr.strip()}") - if result.returncode != 0: - print(f"FAIL: exit code {result.returncode}") - return False - return "PASS" in result.stdout + FILE *fopen(const char *restrict path, const char *restrict mode) { + static FILE *(*real_fopen)(const char *, const char *) = NULL; + if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); + if (is_blocked(path)) { errno = ENOENT; return NULL; } + return real_fopen(path, mode); + } + """) + with tempfile.TemporaryDirectory() as tmpdir: + shim_path = os.path.join(tmpdir, "hide_sysfs.c") + shim_so = os.path.join(tmpdir, "hide_sysfs.so") -def main(): - results = {} + with open(shim_path, "w") as f: + f.write(shim_source) - results["safe_logging"] = test_safe_logging_pattern() - results["sysconf_fallback"] = test_sysconf_fallback() - results["ld_preload_shim"] = test_sysfs_hide_with_ld_preload() - results["ort_import"] = test_ort_import_with_hidden_sysfs() + result = subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", shim_so, shim_path, "-ldl"], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, f"Shim compilation failed: {result.stderr}") - print() - print("=" * 60) - print("Summary") - print("=" * 60) - all_pass = True - for name, result in results.items(): - if result is None: - status = "SKIP" - elif result: - status = "PASS" - else: - status = "FAIL" - all_pass = False - print(f" {name}: {status}") + env = os.environ.copy() + env["LD_PRELOAD"] = shim_so - return 0 if all_pass else 1 + # Try importing onnxruntime with hidden sysfs + ort_script = ( + "import onnxruntime; print('PASS: onnxruntime imported successfully'); " + "print(f'Version: {onnxruntime.__version__}'); " + "print(f'Providers: {onnxruntime.get_available_providers()}')" + ) + result = subprocess.run( + [sys.executable, "-c", ort_script], + check=False, + capture_output=True, + text=True, + timeout=60, + env=env, + ) + self.assertEqual(result.returncode, 0, f"exit code {result.returncode}: {result.stderr}") + self.assertIn("PASS", result.stdout) if __name__ == "__main__": - sys.exit(main()) + unittest.main() From 110e7ced733ce46b14106eace06607708dc29811 Mon Sep 17 00:00:00 2001 From: Tianlei Wu Date: Tue, 28 Apr 2026 15:14:33 -0700 Subject: [PATCH 3/3] intercept open in test --- .../common/test_cpuinfo_sysfs_fallback.py | 205 ++++++++++++++++-- 1 file changed, 188 insertions(+), 17 deletions(-) diff --git a/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py b/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py index 2ab1271f54595..12511512314a5 100644 --- a/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py +++ b/onnxruntime/test/common/test_cpuinfo_sysfs_fallback.py @@ -110,7 +110,9 @@ class LoggingManager { self.assertEqual(result.returncode, 0, f"Compilation failed: {result.stderr}") result = subprocess.run([exe_path], check=False, capture_output=True, text=True, timeout=10) - self.assertEqual(result.returncode, 0, f"Program crashed with exit code {result.returncode}: {result.stderr}") + self.assertEqual( + result.returncode, 0, f"Program crashed with exit code {result.returncode}: {result.stderr}" + ) self.assertIn("PASS", result.stdout) finally: os.unlink(src_path) @@ -202,9 +204,9 @@ def test_sysconf_fallback(self): def test_sysfs_hide_with_ld_preload(self): """Verify LD_PRELOAD shim can hide sysfs files. - This compiles a small shim that intercepts fopen to return ENOENT - for /sys/devices/system/cpu/{possible,present}, then runs a test program - that tries to read those files. + This compiles a small shim that intercepts open-family calls to return + ENOENT for /sys/devices/system/cpu/{possible,present}, then runs a test + program that opens those files. """ _require_linux() _require_gcc() @@ -213,8 +215,15 @@ def test_sysfs_hide_with_ld_preload(self): #define _GNU_SOURCE #include #include - #include + #include + #include #include + #include + #include + +#ifndef O_TMPFILE +#define O_TMPFILE 0 +#endif static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; @@ -223,6 +232,86 @@ def test_sysfs_hide_with_ld_preload(self): return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); } + static mode_t get_mode_if_needed(int flags, va_list args) { + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) ? va_arg(args, mode_t) : 0; + } + + int open(const char *path, int flags, ...) { + static int (*real_open)(const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_open) real_open = dlsym(RTLD_NEXT, "open"); + if (is_blocked(path)) { + errno = ENOENT; + return -1; + } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_open(path, flags, mode) + : real_open(path, flags); + } + + int open64(const char *path, int flags, ...) { + static int (*real_open64)(const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_open64) real_open64 = dlsym(RTLD_NEXT, "open64"); + if (is_blocked(path)) { + errno = ENOENT; + return -1; + } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_open64(path, flags, mode) + : real_open64(path, flags); + } + + int openat(int dirfd, const char *path, int flags, ...) { + static int (*real_openat)(int, const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_openat) real_openat = dlsym(RTLD_NEXT, "openat"); + if (path && is_blocked(path)) { + errno = ENOENT; + return -1; + } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_openat(dirfd, path, flags, mode) + : real_openat(dirfd, path, flags); + } + + int openat64(int dirfd, const char *path, int flags, ...) { + static int (*real_openat64)(int, const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_openat64) real_openat64 = dlsym(RTLD_NEXT, "openat64"); + if (path && is_blocked(path)) { + errno = ENOENT; + return -1; + } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_openat64(dirfd, path, flags, mode) + : real_openat64(dirfd, path, flags); + } + FILE *fopen(const char *restrict path, const char *restrict mode) { static FILE *(*real_fopen)(const char *, const char *) = NULL; if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); @@ -236,28 +325,36 @@ def test_sysfs_hide_with_ld_preload(self): """) test_source = textwrap.dedent(r""" - #include #include + #include + #include #include + #include + + static int try_open(const char *path) { + int fd = open(path, O_RDONLY); + if (fd >= 0) { + close(fd); + } + return fd; + } int main() { - FILE *f; + int fd; int pass = 1; - f = fopen("/sys/devices/system/cpu/possible", "r"); - if (f != NULL) { + fd = try_open("/sys/devices/system/cpu/possible"); + if (fd >= 0) { printf("FAIL: /sys/devices/system/cpu/possible should be blocked\n"); - fclose(f); pass = 0; } else { printf("OK: /sys/devices/system/cpu/possible blocked (errno=%d: %s)\n", errno, strerror(errno)); } - f = fopen("/sys/devices/system/cpu/present", "r"); - if (f != NULL) { + fd = try_open("/sys/devices/system/cpu/present"); + if (fd >= 0) { printf("FAIL: /sys/devices/system/cpu/present should be blocked\n"); - fclose(f); pass = 0; } else { printf("OK: /sys/devices/system/cpu/present blocked (errno=%d: %s)\n", @@ -265,12 +362,11 @@ def test_sysfs_hide_with_ld_preload(self): } // Verify other files still work - f = fopen("/proc/cpuinfo", "r"); - if (f == NULL) { + fd = try_open("/proc/cpuinfo"); + if (fd < 0) { printf("WARN: /proc/cpuinfo not accessible (may be OK in some envs)\n"); } else { printf("OK: /proc/cpuinfo still accessible\n"); - fclose(f); } if (pass) { @@ -334,8 +430,15 @@ def test_ort_import_with_hidden_sysfs(self): #define _GNU_SOURCE #include #include - #include + #include + #include #include + #include + #include + +#ifndef O_TMPFILE +#define O_TMPFILE 0 +#endif static const char *CPU_POSSIBLE = "/sys/devices/system/cpu/possible"; static const char *CPU_PRESENT = "/sys/devices/system/cpu/present"; @@ -344,6 +447,74 @@ def test_ort_import_with_hidden_sysfs(self): return (strcmp(path, CPU_POSSIBLE) == 0 || strcmp(path, CPU_PRESENT) == 0); } + static mode_t get_mode_if_needed(int flags, va_list args) { + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) ? va_arg(args, mode_t) : 0; + } + + int open(const char *path, int flags, ...) { + static int (*real_open)(const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_open) real_open = dlsym(RTLD_NEXT, "open"); + if (is_blocked(path)) { errno = ENOENT; return -1; } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_open(path, flags, mode) + : real_open(path, flags); + } + + int open64(const char *path, int flags, ...) { + static int (*real_open64)(const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_open64) real_open64 = dlsym(RTLD_NEXT, "open64"); + if (is_blocked(path)) { errno = ENOENT; return -1; } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_open64(path, flags, mode) + : real_open64(path, flags); + } + + int openat(int dirfd, const char *path, int flags, ...) { + static int (*real_openat)(int, const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_openat) real_openat = dlsym(RTLD_NEXT, "openat"); + if (path && is_blocked(path)) { errno = ENOENT; return -1; } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_openat(dirfd, path, flags, mode) + : real_openat(dirfd, path, flags); + } + + int openat64(int dirfd, const char *path, int flags, ...) { + static int (*real_openat64)(int, const char *, int, ...) = NULL; + va_list args; + mode_t mode = 0; + + if (!real_openat64) real_openat64 = dlsym(RTLD_NEXT, "openat64"); + if (path && is_blocked(path)) { errno = ENOENT; return -1; } + + va_start(args, flags); + mode = get_mode_if_needed(flags, args); + va_end(args); + return ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) + ? real_openat64(dirfd, path, flags, mode) + : real_openat64(dirfd, path, flags); + } + FILE *fopen(const char *restrict path, const char *restrict mode) { static FILE *(*real_fopen)(const char *, const char *) = NULL; if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen");