From 00a22fb35446a0a83ab5aafb8d19b9c8393b9a81 Mon Sep 17 00:00:00 2001 From: Matt Schwager Date: Fri, 22 May 2026 10:25:04 -0400 Subject: [PATCH 1/3] Add support for C extension fuzzing on macOS --- .github/workflows/builds.yaml | 20 ++++++++ README.md | 2 +- .../native_extension_example/dummy.cc | 43 ++++++++++++++++ .../native_extension_example/fuzzer.py | 30 +++++++++++ .../native_extension_example/setup.py | 51 +++++++++++++++++++ native_extension_fuzzing.md | 28 +++++++--- setup.py | 42 +++++++++------ setup_utils/merge_libfuzzer_sanitizer.sh | 22 ++++++-- 8 files changed, 210 insertions(+), 28 deletions(-) create mode 100644 example_fuzzers/native_extension_example/dummy.cc create mode 100644 example_fuzzers/native_extension_example/fuzzer.py create mode 100644 example_fuzzers/native_extension_example/setup.py diff --git a/.github/workflows/builds.yaml b/.github/workflows/builds.yaml index 71d09361..a5d4f53c 100644 --- a/.github/workflows/builds.yaml +++ b/.github/workflows/builds.yaml @@ -56,3 +56,23 @@ jobs: run: pip3 install virtualenv - name: Build Atheris & run tests run: PYTHON=python ./run_tests.sh + test-macos: + runs-on: macos-latest + strategy: + matrix: + python-version: ["3.13"] + llvm-version: ["21"] + steps: + - uses: actions/checkout@v4 + - name: Install Homebrew LLVM and Python + run: | + brew install llvm@${{ matrix.llvm-version }} python@${{ matrix.python-version }} + echo "$(brew --prefix llvm@${{ matrix.llvm-version }})/bin" >> "$GITHUB_PATH" + echo "$(brew --prefix python@${{ matrix.python-version }})/libexec/bin" >> "$GITHUB_PATH" + - name: Install build deps + run: python3 -m pip install virtualenv + - name: Build Atheris & run tests + env: + CC: clang + CXX: clang++ + run: PYTHON=python3 ./run_tests.sh diff --git a/README.md b/README.md index 11235353..7235af52 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you don't have `clang` installed or it's too old, you'll need to download and #### Mac -Apple Clang doesn't come with libFuzzer, so you'll need to install a new version of LLVM from head. Follow the instructions in Installing Against New LLVM below. +Apple Clang doesn't come with libFuzzer, so you'll need a non-Apple LLVM. The easiest option is Homebrew (`brew install llvm`). Alternatively, follow the instructions in "Installing Against New LLVM" below to build LLVM from source. #### Installing Against New LLVM diff --git a/example_fuzzers/native_extension_example/dummy.cc b/example_fuzzers/native_extension_example/dummy.cc new file mode 100644 index 00000000..2eec12dd --- /dev/null +++ b/example_fuzzers/native_extension_example/dummy.cc @@ -0,0 +1,43 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include + +namespace py = pybind11; + +void process(py::bytes data) { + char* buf = nullptr; + Py_ssize_t size = 0; + if (PyBytes_AsStringAndSize(data.ptr(), &buf, &size) != 0) return; + if (size != 4) return; + const uint8_t* p = reinterpret_cast(buf); + if (p[0] != 'F') return; + if (p[1] != 'U') return; + if (p[2] != 'Z') return; + if (p[3] != 'Z') return; + + char* volatile ptr = static_cast(std::malloc(128)); + ptr[0] = 'x'; + std::free(ptr); + volatile char boom = ptr[0]; + (void)boom; +} + +PYBIND11_MODULE(dummy, m) { + m.doc() = "Intentionally buggy native extension for Atheris demos."; + m.def("process", &process); +} diff --git a/example_fuzzers/native_extension_example/fuzzer.py b/example_fuzzers/native_extension_example/fuzzer.py new file mode 100644 index 00000000..d2e58b62 --- /dev/null +++ b/example_fuzzers/native_extension_example/fuzzer.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import atheris + +import dummy + + +@atheris.instrument_func +def TestOneInput(data): + dummy.process(data) + + +atheris.Setup(sys.argv, TestOneInput) +atheris.Fuzz() diff --git a/example_fuzzers/native_extension_example/setup.py b/example_fuzzers/native_extension_example/setup.py new file mode 100644 index 00000000..fb794d39 --- /dev/null +++ b/example_fuzzers/native_extension_example/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/python3 + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import setup, Extension +import sys + + +class get_pybind_include(object): + + def __str__(self): + import pybind11 + return pybind11.get_include() + + +extra_compile_args = ['-std=c++17'] +extra_link_args = [] +if sys.platform == 'darwin': + darwin_opts = ['-stdlib=libc++', '-mmacosx-version-min=10.7'] + extra_compile_args += darwin_opts + extra_link_args += darwin_opts + +setup( + name='dummy', + version='0.0.1', + description='Intentionally buggy native extension for Atheris demos', + ext_modules=[ + Extension( + 'dummy', + ['dummy.cc'], + include_dirs=[get_pybind_include()], + language='c++', + extra_compile_args=extra_compile_args, + extra_link_args=extra_link_args, + ), + ], + setup_requires=['pybind11>=2.5.0'], + zip_safe=False, +) diff --git a/native_extension_fuzzing.md b/native_extension_fuzzing.md index f1fedfea..bdd28926 100644 --- a/native_extension_fuzzing.md +++ b/native_extension_fuzzing.md @@ -27,23 +27,37 @@ are the two options: ### Option A: Sanitizer+libFuzzer preloads -If you can use this option, we recommend it; it is significantly easier than option #2. (However, this option is not yet supported on Mac). When Atheris is installed, it attempts to generate custom ASan and UBSan shared libraries that have libFuzzer linked in. You can find these libraries in the directory returned by this command: +If you can use this option, we recommend it; it is significantly easier than option #2. When Atheris is installed, it attempts to generate custom ASan and UBSan shared libraries that have libFuzzer linked in. You can find these libraries in the directory returned by this command: ``` python -c "import atheris; print(atheris.path())" ``` -These files will be called: - - `asan_with_fuzzer.so` - - `ubsan_with_fuzzer.so` - - `ubsan_cxx_with_fuzzer.so` +On Linux these files will be called: -If these files are present, it means Atheris successfully generated the files at installation time, and you can use this option. Simply `LD_PRELOAD` the right `.so` file, and you're good to go. Here's a complete example: +- `asan_with_fuzzer.so` +- `ubsan_with_fuzzer.so` +- `ubsan_cxx_with_fuzzer.so` + +On macOS they use the `.dylib` extension (and there is no separate `ubsan_cxx_with_fuzzer.dylib` — the standard ubsan dylib covers C++ checks): + +- `asan_with_fuzzer.dylib` +- `ubsan_with_fuzzer.dylib` + +If these files are present, it means Atheris successfully generated the files at installation time, and you can use this option. Simply preload the right library, and you're good to go. + +Linux example: ``` LD_PRELOAD="$(python -c "import atheris; print(atheris.path())")/asan_with_fuzzer.so" python ./my_fuzzer.py ``` +macOS example: + +``` +DYLD_INSERT_LIBRARIES="$(python -c "import atheris; print(atheris.path())")/asan_with_fuzzer.dylib" python ./my_fuzzer.py +``` + ### Option B: Linking libFuzzer into Python This option doesn't rely on these custom shared libraries; instead, it involves building a modified CPython. We provide a script and patch file that attempts to do this @@ -105,5 +119,5 @@ Python is known to leak certain data, such as at interpreter initialization time ## What if I'm not using a Sanitizer? -While we recommend that you use a sanitizer when fuzzing native code, it's not mandatory. If you'd like to use Atheris to fuzz native code without a sanitizer, you should still build your extension with `-fsanitize=fuzzer-no-link`, and still `LD_PRELOAD` `asan_with_fuzzer.so`. Just remove the `address` entries from +While we recommend that you use a sanitizer when fuzzing native code, it's not mandatory. If you'd like to use Atheris to fuzz native code without a sanitizer, you should still build your extension with `-fsanitize=fuzzer-no-link`, and still preload `asan_with_fuzzer.so` (or `asan_with_fuzzer.dylib` via `DYLD_INSERT_LIBRARIES` on macOS). Just remove the `address` entries from the build command. diff --git a/setup.py b/setup.py index 9858ec8c..007ac884 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,10 @@ no_libfuzzer_error = """Failed to find libFuzzer; set either $CLANG_BIN to point to your Clang binary, or $LIBFUZZER_LIB to point directly to your libFuzzer .a file. If needed, """ + clang_install_instructions -if sys.platform == "darwin": +IS_MACOS = sys.platform == "darwin" +SHLIB_EXT = "dylib" if IS_MACOS else "so" + +if IS_MACOS: too_old_error = ("Your libFuzzer version is too old.\nPlease" + clang_install_instructions + "Do not use Apple " "Clang; Apple Clang does not come with libFuzzer.") @@ -57,6 +60,14 @@ "libFuzzer.\nPlease " + clang_install_instructions) +def sanitizer_lib_path(libfuzzer_path, sanitizer): + if IS_MACOS: + macos_name = {"asan": "asan", "ubsan_standalone": "ubsan"}[sanitizer] + return libfuzzer_path.replace("fuzzer_no_main_osx.a", + "{}_osx_dynamic.dylib".format(macos_name)) + return libfuzzer_path.replace(".fuzzer_no_main", "." + sanitizer) + + class PybindIncludeGetter(object): """Helper class to determine the pybind11 include path. @@ -349,7 +360,7 @@ def build_extensions(self): orig_libfuzzer_name = os.path.basename(libfuzzer) version = check_libfuzzer_version(libfuzzer) - if sys.platform == "darwin" and version != "up-to-date": + if IS_MACOS and version != "up-to-date": raise RuntimeError(too_old_error) if version == "outdated-unrecoverable": @@ -380,7 +391,7 @@ def build_extensions(self): ] l_opts = [] - if sys.platform == "darwin": + if IS_MACOS: darwin_opts = ["-stdlib=libc++", "-mmacosx-version-min=10.7"] c_opts += darwin_opts l_opts += darwin_opts @@ -409,26 +420,27 @@ def build_extensions(self): sys.stderr.write("\n") # Deploy versions of ASan and UBSan that have been merged with libFuzzer - asan_name = orig_libfuzzer.replace(".fuzzer_no_main", ".asan") - merged_asan_name = "asan_with_fuzzer.so" + asan_name = sanitizer_lib_path(orig_libfuzzer, "asan") + merged_asan_name = "asan_with_fuzzer." + SHLIB_EXT self.merge_deploy_libfuzzer_sanitizer( libfuzzer, asan_name, merged_asan_name, "asan_preinit.cc.o asan_preinit.cpp.o") - ubsan_name = orig_libfuzzer.replace(".fuzzer_no_main", ".ubsan_standalone") - merged_ubsan_name = "ubsan_with_fuzzer.so" + ubsan_name = sanitizer_lib_path(orig_libfuzzer, "ubsan_standalone") + merged_ubsan_name = "ubsan_with_fuzzer." + SHLIB_EXT self.merge_deploy_libfuzzer_sanitizer( libfuzzer, ubsan_name, merged_ubsan_name, "ubsan_init_standalone_preinit.cc.o ubsan_init_standalone_preinit.cpp.o" ) - ubsanxx_name = orig_libfuzzer.replace(".fuzzer_no_main", - ".ubsan_standalone_cxx") - merged_ubsanxx_name = "ubsan_cxx_with_fuzzer.so" - self.merge_deploy_libfuzzer_sanitizer( - libfuzzer, ubsanxx_name, merged_ubsanxx_name, - "ubsan_init_standalone_preinit.cc.o ubsan_init_standalone_preinit.cpp.o" - ) + if not IS_MACOS: + ubsanxx_name = orig_libfuzzer.replace(".fuzzer_no_main", + ".ubsan_standalone_cxx") + merged_ubsanxx_name = "ubsan_cxx_with_fuzzer.so" + self.merge_deploy_libfuzzer_sanitizer( + libfuzzer, ubsanxx_name, merged_ubsanxx_name, + "ubsan_init_standalone_preinit.cc.o ubsan_init_standalone_preinit.cpp.o" + ) def deploy_file(self, name, target_filename): atheris = self.get_ext_fullpath("atheris") @@ -473,4 +485,4 @@ def merge_deploy_libfuzzer_sanitizer(self, libfuzzer, lib_name, setup_requires=["pybind11>=2.5.0"], cmdclass={"build_ext": BuildExt}, zip_safe=False, -) \ No newline at end of file +) diff --git a/setup_utils/merge_libfuzzer_sanitizer.sh b/setup_utils/merge_libfuzzer_sanitizer.sh index 81be4c7f..47c4ba15 100755 --- a/setup_utils/merge_libfuzzer_sanitizer.sh +++ b/setup_utils/merge_libfuzzer_sanitizer.sh @@ -19,9 +19,8 @@ libfuzzer="$1" sanitizer="$2" strip_preinit="$3" +uname="$(uname)" tmpdir="$(mktemp -d)" -tmp_sanitizer="${tmpdir}/sanitizer.a" -tmp_merged="${tmpdir}/sanitizer.so" if [ -z "$CXX" ]; then if which clang++ > /dev/null 2>&1; then @@ -31,11 +30,24 @@ if [ -z "$CXX" ]; then fi fi -cp "$sanitizer" "$tmp_sanitizer" +if [[ "$uname" == "Darwin" ]]; then + tmp_merged="${tmpdir}/sanitizer.dylib" + "$CXX" -dynamiclib \ + -Wl,-force_load,"$libfuzzer" \ + "$sanitizer" \ + -lpthread -lc++ \ + -Wl,-install_name,@rpath/$(basename "$tmp_merged") \ + -o "$tmp_merged" +else + tmp_sanitizer="${tmpdir}/sanitizer.a" + tmp_merged="${tmpdir}/sanitizer.so" -ar d "$tmp_sanitizer" $strip_preinit # Intentionally not quoted + cp "$sanitizer" "$tmp_sanitizer" -"$CXX" -Wl,--whole-archive "$libfuzzer" "$tmp_sanitizer" -Wl,--no-whole-archive -lpthread -ldl -shared -o "$tmp_merged" + ar d "$tmp_sanitizer" $strip_preinit # Intentionally not quoted + + "$CXX" -Wl,--whole-archive "$libfuzzer" "$tmp_sanitizer" -Wl,--no-whole-archive -lpthread -ldl -shared -o "$tmp_merged" +fi echo "$tmp_merged" exit 0 From 74c8bf0407d9a00ea11cad3aa6a89f3a526a4af4 Mon Sep 17 00:00:00 2001 From: Matt Schwager Date: Fri, 22 May 2026 10:49:47 -0400 Subject: [PATCH 2/3] Use stdlib venv instead of third-party virtualenv --- .github/workflows/builds.yaml | 6 +----- run_tests.sh | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builds.yaml b/.github/workflows/builds.yaml index a5d4f53c..2fe3e2a5 100644 --- a/.github/workflows/builds.yaml +++ b/.github/workflows/builds.yaml @@ -52,11 +52,9 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' - - name: Install virtualenv - run: pip3 install virtualenv - name: Build Atheris & run tests run: PYTHON=python ./run_tests.sh - test-macos: + macos: runs-on: macos-latest strategy: matrix: @@ -69,8 +67,6 @@ jobs: brew install llvm@${{ matrix.llvm-version }} python@${{ matrix.python-version }} echo "$(brew --prefix llvm@${{ matrix.llvm-version }})/bin" >> "$GITHUB_PATH" echo "$(brew --prefix python@${{ matrix.python-version }})/libexec/bin" >> "$GITHUB_PATH" - - name: Install build deps - run: python3 -m pip install virtualenv - name: Build Atheris & run tests env: CC: clang diff --git a/run_tests.sh b/run_tests.sh index d2eab47f..50096023 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -18,7 +18,7 @@ cp -r . "${TMP_DIR?}" cd "${TMP_DIR?}" # Set up virtual env -"$PYTHON" -m virtualenv . +"$PYTHON" -m venv . source bin/activate # After this, use `python` to get the venv, not $PYTHON python -m pip install setuptools pybind11 python -m pip install . --no-build-isolation From 8b7159d3dc84eae980caa0f9b3ddefbdf7b3cb77 Mon Sep 17 00:00:00 2001 From: Matt Schwager Date: Fri, 22 May 2026 11:03:20 -0400 Subject: [PATCH 3/3] Don't build libprotobuf_mutator on macOS, it's broken --- run_tests.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 50096023..4f051dcd 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -20,9 +20,13 @@ cd "${TMP_DIR?}" # Set up virtual env "$PYTHON" -m venv . source bin/activate # After this, use `python` to get the venv, not $PYTHON -python -m pip install setuptools pybind11 +python -m pip install setuptools pybind11 wheel python -m pip install . --no-build-isolation -(cd contrib/libprotobuf_mutator && python -m pip install . --no-build-isolation) +if [[ "$(uname)" != "Darwin" ]]; then + # libprotobuf_mutator's bundled zlib build is currently broken on macOS + # SDKs (zlib's fdopen macro collides with stdio.h). Skip on Darwin. + (cd contrib/libprotobuf_mutator && python -m pip install . --no-build-isolation) +fi python -m pip install PyInstaller python -m pip install protobuf