diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e16679..352b934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,4 +35,9 @@ jobs: with: cmake-version: '3.31.0' platforms: "['Linux']" - configurations: "['RelWithDebInfo', 'Debug']" \ No newline at end of file + configurations: "['RelWithDebInfo', 'Debug']" + # Sanitizer tests (Clang + ASAN/UBSAN on Linux) + sanitizer: + uses: ./.github/workflows/sanitizer.yml + with: + cmake-version: '3.31.0' \ No newline at end of file diff --git a/.github/workflows/sanitizer.yml b/.github/workflows/sanitizer.yml new file mode 100644 index 0000000..7d08cca --- /dev/null +++ b/.github/workflows/sanitizer.yml @@ -0,0 +1,60 @@ +name: 'sanitizer tests' + +on: + workflow_call: + inputs: + cmake-version: + required: true + type: string + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup CMake + uses: jwlawson/actions-setup-cmake@v2.0.2 + with: + cmake-version: ${{ inputs.cmake-version }} + + - name: Install Clang + run: | + sudo apt-get update -qq + sudo apt-get install -y clang lld + + - name: CMake Configure + env: + CC: clang + CXX: clang++ + run: > + cmake -G "Unix Makefiles" + -B ./Built/Int/cmake_Linux_Sanitizer + -D CMAKE_BUILD_TYPE=Debug + -D CMAKE_C_COMPILER=clang + -D CMAKE_CXX_COMPILER=clang++ + -D CMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" + -D CMAKE_C_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" + -D CMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" + -D CMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" + -D ENABLE_SAMPLES=OFF + . + + - name: CMake Build + run: cmake --build . --target install --config Debug + working-directory: ./Built/Int/cmake_Linux_Sanitizer + + - name: Running Unit Tests + run: ./GLTFSDK.Test --gtest_output=xml:GLTFSDK.Test.Sanitizer.log + working-directory: ./Built/Out/Linux/Debug/GLTFSDK.Test + env: + ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1" + UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1" + + - uses: actions/upload-artifact@v4 + name: "Upload test results" + if: always() + with: + name: GLTFSDK.Test.Sanitizer + path: ${{ github.workspace }}/Built/Out/Linux/Debug/GLTFSDK.Test/GLTFSDK.Test.Sanitizer.log diff --git a/External/RapidJSON/CMakeRapidJSONDownload.txt.in b/External/RapidJSON/CMakeRapidJSONDownload.txt.in index 9d00253..ed9135e 100644 --- a/External/RapidJSON/CMakeRapidJSONDownload.txt.in +++ b/External/RapidJSON/CMakeRapidJSONDownload.txt.in @@ -8,6 +8,7 @@ ExternalProject_Add(RapidJSON GIT_TAG 232389d4f1012dddec4ef84861face2d2ba85709 SOURCE_DIR "${CMAKE_BINARY_DIR}/RapidJSON-src" BINARY_DIR "${CMAKE_BINARY_DIR}/RapidJSON-build" + PATCH_COMMAND "${CMAKE_COMMAND}" -DSOURCE_DIR=${CMAKE_BINARY_DIR}/RapidJSON-src -P "${CMAKE_CURRENT_SOURCE_DIR}/patches/fix-null-allocator-deref.cmake" CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/External/RapidJSON/patches/fix-null-allocator-deref.cmake b/External/RapidJSON/patches/fix-null-allocator-deref.cmake new file mode 100644 index 0000000..f7cd1ea --- /dev/null +++ b/External/RapidJSON/patches/fix-null-allocator-deref.cmake @@ -0,0 +1,34 @@ +# fix-null-allocator-deref.cmake +# +# Patches RapidJSON schema.h to fix CWE-476: null pointer dereference in +# EndMissingDependentProperties(). Replaces calls to +# GetInvalidSchemaPointer().GetAllocator() (which dereferences a null +# allocator_ pointer) with GetStateAllocator() (which is always initialized). +# +# Usage: cmake -DSOURCE_DIR= -P fix-null-allocator-deref.cmake + +if(NOT DEFINED SOURCE_DIR) + message(FATAL_ERROR "SOURCE_DIR must be defined") +endif() + +set(SCHEMA_FILE "${SOURCE_DIR}/include/rapidjson/schema.h") + +if(NOT EXISTS "${SCHEMA_FILE}") + message(FATAL_ERROR "schema.h not found at ${SCHEMA_FILE}") +endif() + +file(READ "${SCHEMA_FILE}" content) + +string(FIND "${content}" "&GetInvalidSchemaPointer().GetAllocator()" match_pos) +if(match_pos EQUAL -1) + message(STATUS "Patch already applied or pattern not found in schema.h") + return() +endif() + +string(REPLACE + "&GetInvalidSchemaPointer().GetAllocator()" + "&GetStateAllocator()" + content "${content}") + +file(WRITE "${SCHEMA_FILE}" "${content}") +message(STATUS "Patched schema.h: replaced GetInvalidSchemaPointer().GetAllocator() with GetStateAllocator()") diff --git a/GLTFSDK.Test/Source/DeserializeTests.cpp b/GLTFSDK.Test/Source/DeserializeTests.cpp index f0dfe9b..f316fb3 100644 --- a/GLTFSDK.Test/Source/DeserializeTests.cpp +++ b/GLTFSDK.Test/Source/DeserializeTests.cpp @@ -10,6 +10,32 @@ using namespace glTF::UnitTest; namespace { + // Malformed glTF JSON inputs that trigger schema dependency validation errors. + // These exercise the EndMissingDependentProperties code path in RapidJSON's + // schema validator which previously crashed with a null pointer dereference + // (CWE-476) when GetInvalidSchemaPointer().GetAllocator() was called on a + // pointer with a null allocator. + + // Root-level dependency: "scene" requires "scenes" to be present + const char* c_missingDependentPropertyScenes = R"({ + "asset": {"version": "2.0"}, + "scene": 0 +})"; + + // Accessor-level dependency: "byteOffset" requires "bufferView" to be present + // (same as c_invalidAccessorDependency but kept separate for regression clarity) + const char* c_missingDependentPropertyBufferView = R"({ + "accessors": [ + { + "byteOffset": 0, + "componentType": 5123, + "count": 1, + "type": "SCALAR" + } + ], + "asset": {"version": "2.0"} +})"; + const char* c_validPrimitiveNoIndices = R"({ "meshes": [ { @@ -540,6 +566,26 @@ namespace Microsoft Assert::AreEqual(doc.samplers[1].wrapS, Wrap_MIRRORED_REPEAT, L"Sampler wrapS property was not deserialized correctly"); Assert::AreEqual(doc.samplers[1].wrapT, Wrap_CLAMP_TO_EDGE, L"Sampler wrapT property was not deserialized correctly"); } + + // Regression test for CWE-476: null pointer dereference in RapidJSON + // schema validator's EndMissingDependentProperties(). A missing + // dependent property must throw ValidationException rather than + // crashing with a null pointer dereference. + GLTFSDK_TEST_METHOD(DeserializeTests, DeserializeFail_MissingDependentPropertyScenes) + { + Assert::ExpectException([]() + { + Deserialize(c_missingDependentPropertyScenes); + }); + } + + GLTFSDK_TEST_METHOD(DeserializeTests, DeserializeFail_MissingDependentPropertyBufferView) + { + Assert::ExpectException([]() + { + Deserialize(c_missingDependentPropertyBufferView); + }); + } }; } }