Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/workflows/builds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,23 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install virtualenv
run: pip3 install virtualenv
Comment on lines -55 to -56

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching to the stdlib's venv library avoids this third-party dependency.

- name: Build Atheris & run tests
run: PYTHON=python ./run_tests.sh
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: Build Atheris & run tests
env:
CC: clang
CXX: clang++
run: PYTHON=python3 ./run_tests.sh
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions example_fuzzers/native_extension_example/dummy.cc
Original file line number Diff line number Diff line change
@@ -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 <pybind11/pybind11.h>

#include <cstdint>
#include <cstdlib>

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<const uint8_t*>(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<char*>(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);
}
30 changes: 30 additions & 0 deletions example_fuzzers/native_extension_example/fuzzer.py
Original file line number Diff line number Diff line change
@@ -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()
51 changes: 51 additions & 0 deletions example_fuzzers/native_extension_example/setup.py
Original file line number Diff line number Diff line change
@@ -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,
)
28 changes: 21 additions & 7 deletions native_extension_fuzzing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
10 changes: 7 additions & 3 deletions run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ cp -r . "${TMP_DIR?}"
cd "${TMP_DIR?}"

# Set up virtual env
"$PYTHON" -m virtualenv .
"$PYTHON" -m venv .

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use venv here.

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

Expand Down
42 changes: 27 additions & 15 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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.

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
)
)
22 changes: 17 additions & 5 deletions setup_utils/merge_libfuzzer_sanitizer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Loading