Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fa80dcb
feat: add fuzzing support with Atheris and Docker configuration
danielmarv Apr 19, 2026
9a798e8
feat: add fuzzing support with Atheris and Docker configuration
danielmarv Apr 19, 2026
7ce0849
feat: add fuzzing support with Atheris and Docker configuration
danielmarv Apr 19, 2026
a36dd29
refactor: address CodeRabbit review feedback
danielmarv Apr 19, 2026
5f46918
fix: add timing tolerance to test_generate_with_jitter
danielmarv Apr 19, 2026
677867c
feat: add fuzzing support with Atheris and Docker configuration
danielmarv Apr 19, 2026
c6cfcd6
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 21, 2026
4694c9a
refactor: clean up and formatting in fuzz targets
danielmarv Apr 22, 2026
a884693
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 23, 2026
ec31a1d
refactor: update entity ID fuzz target and improve exception handling…
danielmarv Apr 24, 2026
53388d7
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 24, 2026
06ce436
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 24, 2026
75ec43f
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 25, 2026
302be55
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 25, 2026
96dea7d
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 27, 2026
534b5de
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 27, 2026
dd562fe
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 29, 2026
bd99418
Merge branch 'main' into fuzzle-sdk-br
danielmarv Apr 30, 2026
5c5a2d3
fix: specify exception types for validation errors in TestOneInput
danielmarv Apr 30, 2026
319b9ea
Merge branch 'fuzzle-sdk-br' of https://github.com/danielmarv/hiero-s…
danielmarv Apr 30, 2026
ad9b14f
fix: specify exception types for validation errors in TestOneInput
danielmarv Apr 30, 2026
9ad6e29
fix: specify exception types for validation errors in TestOneInput
danielmarv Apr 30, 2026
d20e0e2
Merge branch 'main' into fuzzle-sdk-br
danielmarv May 1, 2026
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
9 changes: 9 additions & 0 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM gcr.io/oss-fuzz-base/base-builder-python

RUN apt-get update && apt-get install -y --no-install-recommends \
protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*

COPY . $SRC/hiero-sdk-python
WORKDIR $SRC/hiero-sdk-python
COPY .clusterfuzzlite/build.sh $SRC/
32 changes: 32 additions & 0 deletions .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash -eu
Comment thread
exploreriii marked this conversation as resolved.

pip3 install grpcio-tools
Comment thread
danielmarv marked this conversation as resolved.

python3 generate_proto.py

pip3 install .

pip3 install atheris pyinstaller

find "$SRC/hiero-sdk-python/.clusterfuzzlite" -name '*_fuzzer.py' -print0 | while IFS= read -r -d '' fuzzer; do
fuzzer_basename=$(basename -s .py "$fuzzer")
fuzzer_package="${fuzzer_basename}.pkg"

# Bundle the fuzzer and all its dependencies into a single portable package.
pyinstaller --distpath "$OUT" --onefile --name "$fuzzer_package" "$fuzzer"

# Write a thin shell wrapper that ClusterFuzzLite uses to invoke the fuzzer.
# The wrapper preloads the sanitizer library and sets ASAN_OPTIONS.
# LD_PRELOAD is required here because the SDK uses C extensions (grpcio,
# cryptography, protobuf) that must be covered by the sanitizer.
cat > "$OUT/$fuzzer_basename" << EOF
#!/bin/sh
# LLVMFuzzerTestOneInput for fuzzer detection.
this_dir=\$(dirname "\$0")
LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \\
PYCRYPTODOME_DISABLE_DEEPBIND=1 \\
ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \\
"\$this_dir/$fuzzer_package" "\$@"
EOF
chmod +x "$OUT/$fuzzer_basename"
done
55 changes: 55 additions & 0 deletions .clusterfuzzlite/contract_params_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Atheris fuzz target: ContractFunctionParameters ABI encoding."""

from __future__ import annotations

import sys

import atheris


with atheris.instrument_imports():
from eth_abi.exceptions import EncodingTypeError, ValueOutOfBounds

from hiero_sdk_python import ContractFunctionParameters


def TestOneInput(data: bytes) -> None:
"""Feed arbitrary bytes into ContractFunctionParameters encoding paths."""
fdp = atheris.FuzzedDataProvider(data)
choice = fdp.ConsumeIntInRange(0, 9)
params = ContractFunctionParameters()
if choice == 0:
params.add_bool(fdp.ConsumeBool())
elif choice == 1:
# add_address expects a 20-byte hex string or bytes
params.add_address(fdp.ConsumeBytes(20).hex())
elif choice == 2:
params.add_string(fdp.ConsumeUnicodeNoSurrogates(256))
elif choice == 3:
params.add_bytes(fdp.ConsumeBytes(256))
elif choice == 4:
Comment thread
exploreriii marked this conversation as resolved.
params.add_bytes32(fdp.ConsumeBytes(32))
elif choice == 5:
count = fdp.ConsumeIntInRange(0, 8)
params.add_bool_array([fdp.ConsumeBool() for _ in range(count)])
elif choice == 6:
count = fdp.ConsumeIntInRange(0, 8)
params.add_string_array([fdp.ConsumeUnicodeNoSurrogates(64) for _ in range(count)])
elif choice == 7:
count = fdp.ConsumeIntInRange(0, 8)
params.add_bytes_array([fdp.ConsumeBytes(32) for _ in range(count)])
elif choice == 8:
count = fdp.ConsumeIntInRange(0, 8)
params.add_address_array([fdp.ConsumeBytes(20).hex() for _ in range(count)])
else:
count = fdp.ConsumeIntInRange(0, 8)
params.add_bytes32_array([fdp.ConsumeBytes(32) for _ in range(count)])
Comment thread
exploreriii marked this conversation as resolved.
try:
params.to_bytes()
except (TypeError, ValueError, EncodingTypeError, ValueOutOfBounds):
# Malformed fuzz input can trigger expected validation/ABI encoding failures.
pass


atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
34 changes: 34 additions & 0 deletions .clusterfuzzlite/entity_id_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Atheris fuzz target: entity ID string parsing."""

from __future__ import annotations

import sys

import atheris


with atheris.instrument_imports():
from hiero_sdk_python import AccountId, ContractId, FileId, TokenId, TopicId

_CLASSES = (AccountId, TokenId, ContractId, FileId, TopicId)


def _try_parse_entity_id(entity_id_class, text: str) -> None:
try:
entity_id_class.from_string(text)
except (TypeError, ValueError, IndexError, OverflowError):
# Expected errors from malformed string input (parsing, indexing, conversions)
pass


def TestOneInput(data: bytes) -> None:
"""Feed arbitrary strings into entity ID parsers."""
fdp = atheris.FuzzedDataProvider(data)
text = fdp.ConsumeUnicodeNoSurrogates(256)

for cls in _CLASSES:
_try_parse_entity_id(cls, text)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
45 changes: 45 additions & 0 deletions .clusterfuzzlite/keys_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Atheris fuzz target: PrivateKey and PublicKey parsing."""

from __future__ import annotations

import sys
import warnings

import atheris


with atheris.instrument_imports():
from hiero_sdk_python import PrivateKey, PublicKey


def _quiet(fn, *args):
"""Call *fn* with *args*, suppressing UserWarning."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
return fn(*args)


def TestOneInput(data: bytes) -> None:
"""Feed arbitrary bytes/strings into key parsers."""
fdp = atheris.FuzzedDataProvider(data)
choice = fdp.ConsumeIntInRange(0, 3)

try:
if choice == 0:
text = fdp.ConsumeUnicodeNoSurrogates(256)
_quiet(PrivateKey.from_string, text)
elif choice == 1:
raw = fdp.ConsumeBytes(128)
_quiet(PrivateKey.from_bytes, raw)
elif choice == 2:
text = fdp.ConsumeUnicodeNoSurrogates(256)
_quiet(PublicKey.from_string, text)
else:
raw = fdp.ConsumeBytes(128)
_quiet(PublicKey.from_bytes, raw)
except ValueError:
pass


atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
1 change: 1 addition & 0 deletions .clusterfuzzlite/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
language: python
27 changes: 27 additions & 0 deletions .clusterfuzzlite/transaction_from_bytes_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Atheris fuzz target: Transaction.from_bytes deserialization."""

from __future__ import annotations

import sys

import atheris


with atheris.instrument_imports():
from hiero_sdk_python import Transaction


def TestOneInput(data: bytes) -> None:
"""Feed arbitrary bytes into Transaction.from_bytes and re-serialise."""
try:
tx = Transaction.from_bytes(data)
tx.to_bytes()
except Exception:
# Protobuf deserialization and transaction parsing can raise many
# exception types (DecodeError, ValueError, TypeError, KeyError, etc.);
# only unhandled exceptions that escape are reported as fuzzer crashes.
pass
Comment thread
coderabbitai[bot] marked this conversation as resolved.


atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
1 change: 1 addition & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
exclude_paths:
- "tck/**"
- ".clusterfuzzlite/**"

engines:
bandit:
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/clusterfuzzlite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: ClusterFuzzLite

on:
pull_request:
push:
branches:
- main
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
danielmarv marked this conversation as resolved.

permissions: read-all
Comment thread
manishdait marked this conversation as resolved.
Comment thread
exploreriii marked this conversation as resolved.

jobs:
# ── PR fuzzing ────────────────────────────────────────────────────────────
# Runs on every pull request and reports any new crashes found in the
# changed code paths (code-change mode).
PR:
if: github.event_name == 'pull_request'
runs-on: hl-sdk-py-lin-md
concurrency:
group: ${{ github.workflow }}-pr-${{ matrix.sanitizer }}-${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
sanitizer:
- address
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
with:
egress-policy: audit

- name: Build Fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
with:
language: python
github-token: ${{ secrets.GITHUB_TOKEN }}
sanitizer: ${{ matrix.sanitizer }}

- name: Run Fuzzers (${{ matrix.sanitizer }})
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600
mode: code-change
sanitizer: ${{ matrix.sanitizer }}
output-sarif: true

# ── Continuous builds ─────────────────────────────────────────────────────
# Uploads a fresh fuzzer build artifact on every push to main so that PR
# fuzzing can determine whether a crash was newly introduced.
Build:
if: github.event_name == 'push'
runs-on: hl-sdk-py-lin-md
concurrency:
group: ${{ github.workflow }}-build-${{ matrix.sanitizer }}-${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
sanitizer:
- address
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
with:
egress-policy: audit

- name: Build Fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
with:
language: python
sanitizer: ${{ matrix.sanitizer }}
upload-build: true
4 changes: 4 additions & 0 deletions src/hiero_sdk_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .contract.contract_execute_transaction import ContractExecuteTransaction
from .contract.contract_function_parameters import ContractFunctionParameters
from .contract.contract_function_result import ContractFunctionResult
from .contract.contract_id import ContractId
from .contract.contract_info import ContractInfo
from .contract.contract_info_query import ContractInfoQuery
from .contract.contract_update_transaction import ContractUpdateTransaction
Expand All @@ -52,6 +53,7 @@
from .file.file_contents_query import FileContentsQuery
from .file.file_create_transaction import FileCreateTransaction
from .file.file_delete_transaction import FileDeleteTransaction
from .file.file_id import FileId
from .file.file_info import FileInfo
from .file.file_info_query import FileInfoQuery
from .file.file_update_transaction import FileUpdateTransaction
Expand Down Expand Up @@ -249,6 +251,7 @@
# File
"FileCreateTransaction",
"FileAppendTransaction",
"FileId",
"FileInfoQuery",
"FileInfo",
"FileContentsQuery",
Expand All @@ -257,6 +260,7 @@
# Contract
"ContractCreateTransaction",
"ContractCallQuery",
"ContractId",
"ContractInfoQuery",
"ContractBytecodeQuery",
"ContractExecuteTransaction",
Expand Down
Loading