Repository: oneapi-src/unified-memory-framework (language: python)

Issue:
Add fuzz tests
- [x] Code compiles without errors locally
- [x] All tests pass locally
- [x] CI workflows execute properly
<!-- If you have more tasks to do before merging this PR, simply add them here -->

<!-- You can remove these entries, if they don't apply -->
- [x] New tests added
- [x] Extended the README/documentation
- [x] All newly added source files have a license
- [x] All newly added source files are referenced in CMake files

This PR is a first of many related to fuzzing - ref. #528



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index dc32c9c25..c03cd0d0e 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -11,6 +11,43 @@ permissions:
   contents: read
 
 jobs:
+  fuzz-test:
+      name: Fuzz test
+      strategy:
+        fail-fast: false
+        matrix:
+          build_type: [Debug, Release]
+          compiler: [{c: clang, cxx: clang++}]
+      
+      runs-on: ubuntu-latest
+
+      steps:
+      - name: Checkout repository
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+      - name: Install apt packages
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y cmake hwloc libhwloc-dev libnuma-dev libtbb-dev
+
+      - name: Configure CMake
+        run: >
+          cmake
+          -B ${{github.workspace}}/build
+          -DCMAKE_BUILD_TYPE=${{matrix.build_type}}
+          -DCMAKE_C_COMPILER=${{matrix.compiler.c}}
+          -DCMAKE_CXX_COMPILER=${{matrix.compiler.cxx}}
+          -DUMF_TESTS_FAIL_ON_SKIP=ON
+          -DUMF_DEVELOPER_MODE=ON
+          -DUMF_BUILD_FUZZTESTS=ON
+
+      - name: Build
+        run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} --verbose -j$(nproc)
+
+      - name: Fuzz long test
+        working-directory: ${{github.workspace}}/build
+        run: ctest -C ${{matrix.build_type}} --output-on-failure --verbose -L "fuzz-long"
+
   valgrind:
     name: Valgrind
     strategy:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 33ddafe18..597efafd8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,6 +36,7 @@ option(UMF_BUILD_GPU_TESTS "Build UMF GPU tests" OFF)
 option(UMF_BUILD_BENCHMARKS "Build UMF benchmarks" OFF)
 option(UMF_BUILD_BENCHMARKS_MT "Build UMF multithreaded benchmarks" OFF)
 option(UMF_BUILD_EXAMPLES "Build UMF examples" ON)
+option(UMF_BUILD_FUZZTESTS "Build UMF fuzz tests" OFF)
 option(UMF_BUILD_GPU_EXAMPLES "Build UMF GPU examples" OFF)
 option(UMF_DEVELOPER_MODE "Enable developer checks, treats warnings as errors"
        OFF)
@@ -164,6 +165,13 @@ if(USE_MSAN)
                     "prevent reporting false-positives")
     add_sanitizer_flag(memory)
 endif()
+# Fuzzer instrumentation for the whole library
+if(UMF_BUILD_FUZZTESTS
+   AND CMAKE_CXX_COMPILER_ID MATCHES "Clang"
+   AND LINUX)
+    add_compile_options("-fsanitize=fuzzer-no-link")
+    add_link_options("-fsanitize=fuzzer-no-link")
+endif()
 
 # A header only library to specify include directories in transitive
 # dependencies.
diff --git a/README.md b/README.md
index 65e526765..6d850de9e 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,7 @@ List of options provided by CMake:
 | UMF_BUILD_GPU_TESTS | Build UMF GPU tests | ON/OFF | OFF |
 | UMF_BUILD_BENCHMARKS | Build UMF benchmarks | ON/OFF | OFF |
 | UMF_BUILD_EXAMPLES | Build UMF examples | ON/OFF | ON |
+| UMF_BUILD_FUZZTESTS | Build UMF fuzz tests | ON/OFF | OFF |
 | UMF_BUILD_GPU_EXAMPLES | Build UMF GPU examples | ON/OFF | OFF |
 | UMF_DEVELOPER_MODE | Treat warnings as errors and enables additional checks | ON/OFF | OFF |
 | UMF_FORMAT_CODE_STYLE | Add clang, cmake, and black -format-check and -format-apply targets to make | ON/OFF | OFF |


Test patch:
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 84e9ed82e..165c736cb 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -187,6 +187,9 @@ if(LINUX) # OS-specific functions are implemented only for Linux now
         NAME mempolicy
         SRCS memspaces/mempolicy.cpp
         LIBS ${LIBNUMA_LIBRARIES})
+    if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND UMF_BUILD_FUZZTESTS)
+        add_subdirectory(fuzz)
+    endif()
 endif()
 
 if(UMF_BUILD_GPU_TESTS AND UMF_BUILD_LEVEL_ZERO_PROVIDER)
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
new file mode 100644
index 000000000..b6c175aa4
--- /dev/null
+++ b/test/fuzz/CMakeLists.txt
@@ -0,0 +1,26 @@
+# Copyright (C) 2024 Intel Corporation
+# Under the Apache License v2.0 with LLVM Exceptions. See LICENSE.TXT.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+message(STATUS "Fuzzing tests enabled.")
+
+function(add_fuzz_test name label)
+    add_test(
+        NAME "fuzz-${name}"
+        COMMAND fuzztest ${ARGN} -verbosity=1
+        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+    set_tests_properties("fuzz-${name}" PROPERTIES LABELS ${label})
+endfunction()
+
+set(TEST_TARGET_NAME fuzztest)
+add_umf_executable(
+    NAME ${TEST_TARGET_NAME}
+    SRCS umfFuzz.cpp
+    LIBS umf)
+target_link_libraries(${TEST_TARGET_NAME} PRIVATE umf -fsanitize=fuzzer)
+target_compile_options(${TEST_TARGET_NAME} PRIVATE -g -fsanitize=fuzzer)
+target_include_directories(${TEST_TARGET_NAME}
+                           PRIVATE ${UMF_CMAKE_SOURCE_DIR}/include)
+target_link_directories(${TEST_TARGET_NAME} PRIVATE ${LIBHWLOC_LIBRARY_DIRS})
+
+add_fuzz_test(basic_long fuzz-long -max_total_time=600 -seed=1)
diff --git a/test/fuzz/umfFuzz.cpp b/test/fuzz/umfFuzz.cpp
new file mode 100644
index 000000000..997f329b8
--- /dev/null
+++ b/test/fuzz/umfFuzz.cpp
@@ -0,0 +1,220 @@
+// Copyright (C) 2024 Intel Corporation
+// Under the Apache License v2.0 with LLVM Exceptions. See LICENSE.TXT.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "utils.hpp"
+
+namespace fuzz {
+
+constexpr int MAX_PROVIDER_VECTOR_SIZE = 1024;
+constexpr int MAX_POOLS_VECTOR_SIZE = 20;
+constexpr int MAX_POOLS_ALLOC_SIZE = 1 * 1024;      // 1 kB
+constexpr int MAX_PROVIDER_ALLOC_SIZE = 100 * 1024; // 100 kB
+
+int umf_memory_provider_create(TestState &test_state) {
+    std::cout << "Begin creating a provider" << std::endl;
+    umf_memory_provider_ops_t *provider_ops = umfOsMemoryProviderOps();
+    umf_os_memory_provider_params_t params = umfOsMemoryProviderParamsDefault();
+    umf_result_t res =
+        umfMemoryProviderCreate(provider_ops, &params, &test_state.provider);
+
+    if (res != UMF_RESULT_SUCCESS) {
+        std::cout << "Failed to create a memory provider: " << res << std::endl;
+        return -1;
+    }
+    std::cout << "OS memory provider created at " << (void *)test_state.provider
+              << std::endl;
+    return 0;
+}
+
+int umf_memory_provider_alloc(TestState &test_state) {
+    std::cout << "Begin memory_provider_alloc" << std::endl;
+    void *ptr;
+    size_t alloc_size;
+    constexpr size_t alignment = 0;
+
+    if (test_state.provider_memory_allocations.size() >=
+        MAX_PROVIDER_VECTOR_SIZE) {
+        return -1;
+    }
+
+    int ret = test_state.get_next_alloc_size(test_state, alloc_size,
+                                             MAX_PROVIDER_ALLOC_SIZE);
+    if (ret != 0) {
+        std::cout << "Failed to get alloc size" << std::endl;
+        return -1;
+    }
+
+    umf_result_t res = umfMemoryProviderAlloc(test_state.provider, alloc_size,
+                                              alignment, &ptr);
+    if (res != UMF_RESULT_SUCCESS) {
+        std::cout << "Failed to allocate memory from the provider: " << res
+                  << std::endl;
+        return -1;
+    }
+    test_state.provider_memory_allocations.push_back(
+        std::make_pair(ptr, alloc_size));
+    std::cout << "Allocated memory at " << ptr << " with alloc_size "
+              << alloc_size << std::endl;
+    std::cout << "Size of vector with allocated memory from the provider: "
+              << test_state.provider_memory_allocations.size() << std::endl;
+    return 0;
+}
+
+int umf_memory_provider_free(TestState &test_state) {
+    std::cout << "Begin memory_provider_free" << std::endl;
+    if (test_state.provider_memory_allocations.empty()) {
+        std::cout << "No memory allocated" << std::endl;
+        return -1;
+    }
+
+    std::pair<void *, size_t> alloc =
+        test_state.provider_memory_allocations.back();
+    umf_result_t res =
+        umfMemoryProviderFree(test_state.provider, alloc.first, alloc.second);
+
+    if (res != UMF_RESULT_SUCCESS) {
+        std::cout << "Failed to free memory to the provider: " << res
+                  << std::endl;
+        ;
+        return -1;
+    }
+
+    std::cout << "Freed memory from the provider at " << alloc.first
+              << " with alloc_size " << alloc.second << std::endl;
+    test_state.provider_memory_allocations.pop_back();
+    return 0;
+}
+
+int umf_pool_create(TestState &test_state) {
+    if (test_state.pools.size() > MAX_POOLS_VECTOR_SIZE) {
+        std::cout << "Max pools limit reached" << std::endl;
+        return -1;
+    }
+
+    umf_memory_pool_ops_t *pool_ops = umfScalablePoolOps();
+    void *pool_params = NULL;
+    umf_pool_create_flags_t flags = 0;
+    umf_memory_pool_handle_t pool;
+    umf_result_t res =
+        umfPoolCreate(pool_ops, test_state.provider, pool_params, flags, &pool);
+
+    if (res != UMF_RESULT_SUCCESS) {
+        std::cout << "Failed to create a pool: " << res << std::endl;
+        return -1;
+    }
+
+    test_state.pools.insert(std::make_pair(pool, std::vector<void *>()));
+    std::cout << "Scalable memory pool created at " << pool
+              << " and pools available: " << test_state.pools.size()
+              << std::endl;
+    return 0;
+}
+
+int umf_pool_destroy(TestState &test_state) {
+    std::cout << "Begin destroy pool" << std::endl;
+    if (test_state.pools.empty()) {
+        std::cout << "No pools created" << std::endl;
+        return -1;
+    }
+    auto pool = (*test_state.pools.begin()).first;
+    umfPoolDestroy(pool);
+    test_state.pools.erase(pool);
+    std::cout << "Destroyed pool at " << pool << std::endl;
+    return 0;
+}
+
+int umf_pool_malloc(TestState &test_state) {
+    std::cout << "Begin pool_malloc" << std::endl;
+    if (test_state.pools.empty()) {
+        std::cout << "No pools created" << std::endl;
+        return -1;
+    }
+    size_t alloc_size;
+    int ret = test_state.get_next_alloc_size(test_state, alloc_size,
+                                             MAX_POOLS_ALLOC_SIZE);
+    if (ret != 0) {
+        std::cout << "Failed to get next allocation size" << std::endl;
+        return -1;
+    }
+    auto &pool_entry = *test_state.pools.rbegin();
+    void *ptr = umfPoolMalloc(pool_entry.first, alloc_size);
+    if (!ptr) {
+        std::cout
+            << "Failed to allocate memory in the pool with handle address: "
+            << pool_entry.first << std::endl;
+    }
+
+    pool_entry.second.push_back(ptr);
+    std::cout << "Allocated memory at " << ptr
+              << " with allocation size: " << alloc_size << std::endl;
+    return 0;
+}
+
+int umf_free(TestState &test_state) {
+    std::cout << "Begin releasing pool memory" << std::endl;
+    for (auto &pool : test_state.pools) {
+        if (pool.second.empty()) {
+            continue;
+        } else {
+            umfFree(pool.second.back());
+            pool.second.pop_back();
+            std::cout << "Freed memory from the pool at: " << pool.second.back()
+                      << std::endl;
+            break;
+        }
+        std::cout << "No pool memory to free" << std::endl;
+        return -1;
+    }
+    return 0;
+}
+
+void cleanup(TestState &test_state) {
+    std::cout << "Begin cleanup state" << std::endl;
+    for (auto &alloc : test_state.provider_memory_allocations) {
+        umfMemoryProviderFree(test_state.provider, alloc.first, alloc.second);
+    }
+
+    for (auto &pool_entry : test_state.pools) {
+        for (auto &ptr : pool_entry.second) {
+            umfFree(ptr);
+        }
+        umfPoolDestroy(pool_entry.first);
+    }
+    std::cout << "Freed all allocated memory from provider and pools and "
+                 "destroyed all pools"
+              << std::endl;
+    umfMemoryProviderDestroy(test_state.provider);
+    std::cout << "Destroyed the provider" << std::endl;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(uint8_t *data, size_t size) {
+    int next_api_call;
+    auto data_provider = std::make_unique<FuzzedDataProvider>(data, size);
+    TestState test_state(std::move(data_provider));
+    int ret = -1;
+
+    // clang-format off
+    int (*api_wrappers[])(TestState &) = {
+        umf_memory_provider_alloc,
+        umf_memory_provider_free,
+        umf_pool_create,
+        umf_pool_destroy,
+        umf_pool_malloc,
+        umf_free,
+    };
+    // clang-format on
+    umf_memory_provider_create(test_state);
+
+    while ((next_api_call = test_state.get_next_api_call()) != -1) {
+        ret = api_wrappers[next_api_call](test_state);
+        if (ret) {
+            cleanup(test_state);
+            return -1;
+        }
+    }
+
+    cleanup(test_state);
+    return 0;
+}
+} // namespace fuzz
diff --git a/test/fuzz/utils.hpp b/test/fuzz/utils.hpp
new file mode 100644
index 000000000..ec1dda700
--- /dev/null
+++ b/test/fuzz/utils.hpp
@@ -0,0 +1,77 @@
+// Copyright (C) 2024 Intel Corporation
+// Under the Apache License v2.0 with LLVM Exceptions. See LICENSE.TXT.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "umf/pools/pool_scalable.h"
+#include "umf/providers/provider_os_memory.h"
+#include <fuzzer/FuzzedDataProvider.h>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace fuzz {
+
+enum FuzzerAPICall : uint8_t {
+    UMF_MEMORY_PROVIDER_ALLOC,
+    UMF_MEMORY_PROVIDER_FREE,
+    UMF_POOL_CREATE,
+    UMF_POOL_DESTROY,
+    UMF_POOL_MALLOC,
+    UMF_POOL_FREE,
+    kMaxValue = UMF_POOL_FREE,
+};
+
+struct TestState {
+    std::unique_ptr<FuzzedDataProvider> data_provider;
+    umf_memory_provider_handle_t provider;
+    std::vector<std::pair<void *, size_t>> provider_memory_allocations;
+    std::map<umf_memory_pool_handle_t, std::vector<void *>> pools;
+
+    TestState(std::unique_ptr<FuzzedDataProvider> data_provider)
+        : data_provider(std::move(data_provider)) {}
+
+    template <typename IntType> int get_next_input_data(IntType *data) {
+        if (data_provider->remaining_bytes() < sizeof(IntType)) {
+            return -1;
+        }
+        *data = data_provider->ConsumeIntegral<IntType>();
+
+        return 0;
+    }
+
+    template <typename IntType>
+    int get_next_input_data_in_range(IntType *data, IntType min, IntType max) {
+        if (data_provider->remaining_bytes() < sizeof(IntType)) {
+            return -1;
+        }
+        *data = data_provider->ConsumeIntegralInRange<IntType>(min, max);
+
+        return 0;
+    }
+
+    template <typename EnumType> int get_next_input_data_enum(EnumType *data) {
+        if (data_provider->remaining_bytes() < sizeof(EnumType)) {
+            return -1;
+        }
+        *data = data_provider->ConsumeEnum<EnumType>();
+
+        return 0;
+    }
+
+    int get_next_api_call() {
+        FuzzerAPICall next_api_call;
+        return get_next_input_data_enum(&next_api_call) == 0 ? next_api_call
+                                                             : -1;
+    }
+
+    size_t get_next_alloc_size(TestState &state, size_t &alloc_size,
+                               size_t max_alloc_size) {
+        if (state.get_next_input_data_in_range<size_t>(&alloc_size, 0,
+                                                       max_alloc_size) != 0) {
+            return -1;
+        }
+        return 0;
+    }
+};
+} // namespace fuzz