From fa80dcb9b6358e7347cabe62ee8182a59dde7a8a Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Sun, 19 Apr 2026 18:03:48 +0300 Subject: [PATCH 01/11] feat: add fuzzing support with Atheris and Docker configuration Signed-off-by: Ntege Daniel --- .clusterfuzzlite/Dockerfile | 9 +++ .clusterfuzzlite/build.sh | 39 ++++++++++ .clusterfuzzlite/contract_params_fuzzer.py | 50 ++++++++++++ .clusterfuzzlite/entity_id_fuzzer.py | 30 +++++++ .clusterfuzzlite/keys_fuzzer.py | 45 +++++++++++ .clusterfuzzlite/project.yaml | 1 + .../transaction_from_bytes_fuzzer.py | 26 +++++++ .github/workflows/clusterfuzzlite.yml | 78 +++++++++++++++++++ pyproject.toml | 9 +++ 9 files changed, 287 insertions(+) create mode 100644 .clusterfuzzlite/Dockerfile create mode 100644 .clusterfuzzlite/build.sh create mode 100644 .clusterfuzzlite/contract_params_fuzzer.py create mode 100644 .clusterfuzzlite/entity_id_fuzzer.py create mode 100644 .clusterfuzzlite/keys_fuzzer.py create mode 100644 .clusterfuzzlite/project.yaml create mode 100644 .clusterfuzzlite/transaction_from_bytes_fuzzer.py create mode 100644 .github/workflows/clusterfuzzlite.yml diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 000000000..0a89c8dc8 --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -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/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 000000000..730807d55 --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash -eu + +# Install grpcio-tools first so generate_proto.py can compile the protobufs. +pip3 install grpcio-tools + +# Generate the protobuf Python bindings required by the SDK. +python3 generate_proto.py + +# Install the SDK and all runtime dependencies (uses current CFLAGS/CXXFLAGS so +# any C extensions such as grpcio and cryptography are built with the right flags). +pip3 install . + +# Install Atheris (the Python fuzzing engine) and PyInstaller (used to produce +# self-contained fuzzer executables that ClusterFuzzLite can run reliably). +pip3 install atheris pyinstaller + +# Build every *_fuzzer.py target found in the .clusterfuzzlite directory. +for fuzzer in $(find "$SRC/hiero-sdk-python/.clusterfuzzlite" -name '*_fuzzer.py'); 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 diff --git a/.clusterfuzzlite/contract_params_fuzzer.py b/.clusterfuzzlite/contract_params_fuzzer.py new file mode 100644 index 000000000..a1c133ff8 --- /dev/null +++ b/.clusterfuzzlite/contract_params_fuzzer.py @@ -0,0 +1,50 @@ +"""Atheris fuzz target: ContractFunctionParameters ABI encoding.""" + +from __future__ import annotations + +import sys + +import atheris + + +with atheris.instrument_imports(): + 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, 7) + + try: + 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: + 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)]) + else: + count = fdp.ConsumeIntInRange(0, 8) + params.add_bytes_array([fdp.ConsumeBytes(32) for _ in range(count)]) + + params.to_bytes() + + except Exception: + pass + + +atheris.Setup(sys.argv, TestOneInput) +atheris.Fuzz() diff --git a/.clusterfuzzlite/entity_id_fuzzer.py b/.clusterfuzzlite/entity_id_fuzzer.py new file mode 100644 index 000000000..36496fb6f --- /dev/null +++ b/.clusterfuzzlite/entity_id_fuzzer.py @@ -0,0 +1,30 @@ +"""Atheris fuzz target: entity ID string parsing (AccountId, TokenId, ContractId).""" + +from __future__ import annotations + +import sys + +import atheris + + +with atheris.instrument_imports(): + from hiero_sdk_python import AccountId, TokenId + from hiero_sdk_python.contract.contract_id import ContractId + +_CLASSES = (AccountId, TokenId, ContractId) + + +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: + cls.from_string(text) + except Exception: # noqa: PERF203 + pass + + +atheris.Setup(sys.argv, TestOneInput) +atheris.Fuzz() diff --git a/.clusterfuzzlite/keys_fuzzer.py b/.clusterfuzzlite/keys_fuzzer.py new file mode 100644 index 000000000..4c9fbe7b7 --- /dev/null +++ b/.clusterfuzzlite/keys_fuzzer.py @@ -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 Exception: + pass + + +atheris.Setup(sys.argv, TestOneInput) +atheris.Fuzz() diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 000000000..d1ad0ae50 --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: python diff --git a/.clusterfuzzlite/transaction_from_bytes_fuzzer.py b/.clusterfuzzlite/transaction_from_bytes_fuzzer.py new file mode 100644 index 000000000..7e5ce5852 --- /dev/null +++ b/.clusterfuzzlite/transaction_from_bytes_fuzzer.py @@ -0,0 +1,26 @@ +"""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: + # All parsing / validation failures are expected; only unhandled + # exceptions that escape this function are reported as fuzzer crashes. + pass + + +atheris.Setup(sys.argv, TestOneInput) +atheris.Fuzz() diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml new file mode 100644 index 000000000..1d039a7d6 --- /dev/null +++ b/.github/workflows/clusterfuzzlite.yml @@ -0,0 +1,78 @@ +name: ClusterFuzzLite + +on: + pull_request: + paths: + - "**" + push: + branches: + - main + +permissions: read-all + +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: ubuntu-latest + 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: ubuntu-latest + 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 diff --git a/pyproject.toml b/pyproject.toml index aa743c99f..3dc04e4ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,15 @@ eth = [ tck = ["flask>=3.0.0,<4"] +fuzz = [ + # Atheris (Linux + libFuzzer only) and PyInstaller are used exclusively by + # the ClusterFuzzLite Docker build (.clusterfuzzlite/build.sh). + # They are intentionally not part of the standard dev group because Atheris + # requires a libFuzzer-instrumented build environment. + "atheris>=2.3.0", + "pyinstaller>=6.0.0", +] + [dependency-groups] dev = [ "grpcio-tools>=1.76.0,<2", From 9a798e81219a6799c90578e5e010d96c3967a50e Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Sun, 19 Apr 2026 18:20:53 +0300 Subject: [PATCH 02/11] feat: add fuzzing support with Atheris and Docker configuration Signed-off-by: Ntege Daniel --- .github/workflows/clusterfuzzlite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml index 1d039a7d6..f13f5f881 100644 --- a/.github/workflows/clusterfuzzlite.yml +++ b/.github/workflows/clusterfuzzlite.yml @@ -16,7 +16,7 @@ jobs: # changed code paths (code-change mode). PR: if: github.event_name == 'pull_request' - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: ${{ github.workflow }}-pr-${{ matrix.sanitizer }}-${{ github.ref }} cancel-in-progress: true @@ -54,7 +54,7 @@ jobs: # fuzzing can determine whether a crash was newly introduced. Build: if: github.event_name == 'push' - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: ${{ github.workflow }}-build-${{ matrix.sanitizer }}-${{ github.ref }} cancel-in-progress: true From 7ce0849d8b932e849894ae667415ef467b1339fb Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Sun, 19 Apr 2026 19:46:36 +0300 Subject: [PATCH 03/11] feat: add fuzzing support with Atheris and Docker configuration Signed-off-by: Ntege Daniel --- .codacy.yml | 1 + pyproject.toml | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.codacy.yml b/.codacy.yml index d83fecdd1..d54a0239e 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,6 +1,7 @@ --- exclude_paths: - "tck/**" + - ".clusterfuzzlite/**" engines: bandit: diff --git a/pyproject.toml b/pyproject.toml index 3dc04e4ac..aa743c99f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,15 +43,6 @@ eth = [ tck = ["flask>=3.0.0,<4"] -fuzz = [ - # Atheris (Linux + libFuzzer only) and PyInstaller are used exclusively by - # the ClusterFuzzLite Docker build (.clusterfuzzlite/build.sh). - # They are intentionally not part of the standard dev group because Atheris - # requires a libFuzzer-instrumented build environment. - "atheris>=2.3.0", - "pyinstaller>=6.0.0", -] - [dependency-groups] dev = [ "grpcio-tools>=1.76.0,<2", From a36dd2937f77d789b10fa9987140783d909079c6 Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Sun, 19 Apr 2026 20:33:48 +0300 Subject: [PATCH 04/11] refactor: address CodeRabbit review feedback - Use null-delimited find loop in build.sh to safely handle filenames with spaces (SC2044) - Expand contract_params_fuzzer to cover address[] and bytes32[] array encoders (extends coverage from 8 to 10 encoding paths) - Remove redundant paths filter from workflow trigger (paths: ["**"] is equivalent to omitting it) Signed-off-by: Ntege Daniel --- .clusterfuzzlite/build.sh | 2 +- .clusterfuzzlite/contract_params_fuzzer.py | 10 ++++++++-- .github/workflows/clusterfuzzlite.yml | 2 -- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 730807d55..92498653d 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -15,7 +15,7 @@ pip3 install . pip3 install atheris pyinstaller # Build every *_fuzzer.py target found in the .clusterfuzzlite directory. -for fuzzer in $(find "$SRC/hiero-sdk-python/.clusterfuzzlite" -name '*_fuzzer.py'); do +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" diff --git a/.clusterfuzzlite/contract_params_fuzzer.py b/.clusterfuzzlite/contract_params_fuzzer.py index a1c133ff8..beda0911c 100644 --- a/.clusterfuzzlite/contract_params_fuzzer.py +++ b/.clusterfuzzlite/contract_params_fuzzer.py @@ -14,7 +14,7 @@ def TestOneInput(data: bytes) -> None: """Feed arbitrary bytes into ContractFunctionParameters encoding paths.""" fdp = atheris.FuzzedDataProvider(data) - choice = fdp.ConsumeIntInRange(0, 7) + choice = fdp.ConsumeIntInRange(0, 9) try: params = ContractFunctionParameters() @@ -36,9 +36,15 @@ def TestOneInput(data: bytes) -> None: elif choice == 6: count = fdp.ConsumeIntInRange(0, 8) params.add_string_array([fdp.ConsumeUnicodeNoSurrogates(64) for _ in range(count)]) - else: + 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)]) params.to_bytes() diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml index f13f5f881..56390b9e0 100644 --- a/.github/workflows/clusterfuzzlite.yml +++ b/.github/workflows/clusterfuzzlite.yml @@ -2,8 +2,6 @@ name: ClusterFuzzLite on: pull_request: - paths: - - "**" push: branches: - main From 5f469182cbabb23dc58950b35df79c6ea822358a Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Sun, 19 Apr 2026 20:44:25 +0300 Subject: [PATCH 05/11] fix: add timing tolerance to test_generate_with_jitter Relax the lower bound from 3.0 to 2.99 seconds to account for microsecond-level precision variations in timing checks. The test was failing intermittently on Python 3.12 with delta values like 2.9999... which are technically valid but fell just under the strict 3.0 threshold. Signed-off-by: Ntege Daniel --- tests/unit/timestamp_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/timestamp_test.py b/tests/unit/timestamp_test.py index 8e6024d1b..73c34403c 100644 --- a/tests/unit/timestamp_test.py +++ b/tests/unit/timestamp_test.py @@ -191,8 +191,8 @@ def test_generate_with_jitter(): ts = Timestamp.generate(has_jitter=True) delta = time.time() - ts.to_date().timestamp() - # Jitter is explicitly 3-8 seconds backward - assert 3.0 <= delta <= 9.0 + # Jitter is explicitly 3-8 seconds backward (with small tolerance for timing precision) + assert 2.99 <= delta <= 9.0 # Protobuf serialization tests From 677867cb83a0e76fa65e8b5bcf47fd00a6a7c7b9 Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Sun, 19 Apr 2026 20:48:15 +0300 Subject: [PATCH 06/11] feat: add fuzzing support with Atheris and Docker configuration Signed-off-by: Ntege Daniel --- tests/unit/timestamp_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/timestamp_test.py b/tests/unit/timestamp_test.py index 73c34403c..8e6024d1b 100644 --- a/tests/unit/timestamp_test.py +++ b/tests/unit/timestamp_test.py @@ -191,8 +191,8 @@ def test_generate_with_jitter(): ts = Timestamp.generate(has_jitter=True) delta = time.time() - ts.to_date().timestamp() - # Jitter is explicitly 3-8 seconds backward (with small tolerance for timing precision) - assert 2.99 <= delta <= 9.0 + # Jitter is explicitly 3-8 seconds backward + assert 3.0 <= delta <= 9.0 # Protobuf serialization tests From 4694c9ac91a45f68f91f663f48c3c20b2bba4da3 Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Wed, 22 Apr 2026 11:06:10 +0300 Subject: [PATCH 07/11] refactor: clean up and formatting in fuzz targets Signed-off-by: Ntege Daniel --- .clusterfuzzlite/build.sh | 7 ------- .clusterfuzzlite/contract_params_fuzzer.py | 4 ---- 2 files changed, 11 deletions(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 92498653d..406ccb8e9 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -1,20 +1,13 @@ #!/bin/bash -eu -# Install grpcio-tools first so generate_proto.py can compile the protobufs. pip3 install grpcio-tools -# Generate the protobuf Python bindings required by the SDK. python3 generate_proto.py -# Install the SDK and all runtime dependencies (uses current CFLAGS/CXXFLAGS so -# any C extensions such as grpcio and cryptography are built with the right flags). pip3 install . -# Install Atheris (the Python fuzzing engine) and PyInstaller (used to produce -# self-contained fuzzer executables that ClusterFuzzLite can run reliably). pip3 install atheris pyinstaller -# Build every *_fuzzer.py target found in the .clusterfuzzlite directory. 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" diff --git a/.clusterfuzzlite/contract_params_fuzzer.py b/.clusterfuzzlite/contract_params_fuzzer.py index beda0911c..b78f1ce4a 100644 --- a/.clusterfuzzlite/contract_params_fuzzer.py +++ b/.clusterfuzzlite/contract_params_fuzzer.py @@ -15,10 +15,8 @@ def TestOneInput(data: bytes) -> None: """Feed arbitrary bytes into ContractFunctionParameters encoding paths.""" fdp = atheris.FuzzedDataProvider(data) choice = fdp.ConsumeIntInRange(0, 9) - try: params = ContractFunctionParameters() - if choice == 0: params.add_bool(fdp.ConsumeBool()) elif choice == 1: @@ -45,9 +43,7 @@ def TestOneInput(data: bytes) -> None: else: count = fdp.ConsumeIntInRange(0, 8) params.add_bytes32_array([fdp.ConsumeBytes(32) for _ in range(count)]) - params.to_bytes() - except Exception: pass From ec31a1da023afa25aca6a59885f131960c0fe646 Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Fri, 24 Apr 2026 10:00:48 +0300 Subject: [PATCH 08/11] refactor: update entity ID fuzz target and improve exception handling in fuzzers Signed-off-by: Ntege Daniel --- .clusterfuzzlite/entity_id_fuzzer.py | 19 +++++++++++-------- .clusterfuzzlite/keys_fuzzer.py | 2 +- .../transaction_from_bytes_fuzzer.py | 2 +- src/hiero_sdk_python/__init__.py | 4 ++++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.clusterfuzzlite/entity_id_fuzzer.py b/.clusterfuzzlite/entity_id_fuzzer.py index 36496fb6f..87bf8b2d2 100644 --- a/.clusterfuzzlite/entity_id_fuzzer.py +++ b/.clusterfuzzlite/entity_id_fuzzer.py @@ -1,4 +1,4 @@ -"""Atheris fuzz target: entity ID string parsing (AccountId, TokenId, ContractId).""" +"""Atheris fuzz target: entity ID string parsing.""" from __future__ import annotations @@ -8,10 +8,16 @@ with atheris.instrument_imports(): - from hiero_sdk_python import AccountId, TokenId - from hiero_sdk_python.contract.contract_id import ContractId + from hiero_sdk_python import AccountId, ContractId, FileId, TokenId, TopicId -_CLASSES = (AccountId, TokenId, ContractId) +_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): + pass def TestOneInput(data: bytes) -> None: @@ -20,10 +26,7 @@ def TestOneInput(data: bytes) -> None: text = fdp.ConsumeUnicodeNoSurrogates(256) for cls in _CLASSES: - try: - cls.from_string(text) - except Exception: # noqa: PERF203 - pass + _try_parse_entity_id(cls, text) atheris.Setup(sys.argv, TestOneInput) diff --git a/.clusterfuzzlite/keys_fuzzer.py b/.clusterfuzzlite/keys_fuzzer.py index 4c9fbe7b7..c60540bd0 100644 --- a/.clusterfuzzlite/keys_fuzzer.py +++ b/.clusterfuzzlite/keys_fuzzer.py @@ -37,7 +37,7 @@ def TestOneInput(data: bytes) -> None: else: raw = fdp.ConsumeBytes(128) _quiet(PublicKey.from_bytes, raw) - except Exception: + except ValueError: pass diff --git a/.clusterfuzzlite/transaction_from_bytes_fuzzer.py b/.clusterfuzzlite/transaction_from_bytes_fuzzer.py index 7e5ce5852..d2ca4d75b 100644 --- a/.clusterfuzzlite/transaction_from_bytes_fuzzer.py +++ b/.clusterfuzzlite/transaction_from_bytes_fuzzer.py @@ -16,7 +16,7 @@ def TestOneInput(data: bytes) -> None: try: tx = Transaction.from_bytes(data) tx.to_bytes() - except Exception: + except ValueError: # All parsing / validation failures are expected; only unhandled # exceptions that escape this function are reported as fuzzer crashes. pass diff --git a/src/hiero_sdk_python/__init__.py b/src/hiero_sdk_python/__init__.py index 329f6f0a4..b890f3330 100644 --- a/src/hiero_sdk_python/__init__.py +++ b/src/hiero_sdk_python/__init__.py @@ -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 @@ -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 @@ -249,6 +251,7 @@ # File "FileCreateTransaction", "FileAppendTransaction", + "FileId", "FileInfoQuery", "FileInfo", "FileContentsQuery", @@ -257,6 +260,7 @@ # Contract "ContractCreateTransaction", "ContractCallQuery", + "ContractId", "ContractInfoQuery", "ContractBytecodeQuery", "ContractExecuteTransaction", From 5c5a2d3ef3bc3ac4f222b2075ea8aeb2cbdaa036 Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Thu, 30 Apr 2026 19:32:52 +0300 Subject: [PATCH 09/11] fix: specify exception types for validation errors in TestOneInput Signed-off-by: Ntege Daniel --- .clusterfuzzlite/contract_params_fuzzer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.clusterfuzzlite/contract_params_fuzzer.py b/.clusterfuzzlite/contract_params_fuzzer.py index b78f1ce4a..ed4f6bbb6 100644 --- a/.clusterfuzzlite/contract_params_fuzzer.py +++ b/.clusterfuzzlite/contract_params_fuzzer.py @@ -44,7 +44,8 @@ def TestOneInput(data: bytes) -> None: count = fdp.ConsumeIntInRange(0, 8) params.add_bytes32_array([fdp.ConsumeBytes(32) for _ in range(count)]) params.to_bytes() - except Exception: + except (TypeError, ValueError): + # Expected validation errors from malformed input pass From ad9b14f1ffae4a77ca0b75c99eae9ddfebab8701 Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Thu, 30 Apr 2026 19:41:22 +0300 Subject: [PATCH 10/11] fix: specify exception types for validation errors in TestOneInput Signed-off-by: Ntege Daniel --- .clusterfuzzlite/contract_params_fuzzer.py | 60 +++++++++---------- .clusterfuzzlite/entity_id_fuzzer.py | 3 +- .../transaction_from_bytes_fuzzer.py | 7 ++- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/.clusterfuzzlite/contract_params_fuzzer.py b/.clusterfuzzlite/contract_params_fuzzer.py index ed4f6bbb6..73d9b29c3 100644 --- a/.clusterfuzzlite/contract_params_fuzzer.py +++ b/.clusterfuzzlite/contract_params_fuzzer.py @@ -15,38 +15,34 @@ def TestOneInput(data: bytes) -> None: """Feed arbitrary bytes into ContractFunctionParameters encoding paths.""" fdp = atheris.FuzzedDataProvider(data) choice = fdp.ConsumeIntInRange(0, 9) - try: - 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: - 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)]) - params.to_bytes() - except (TypeError, ValueError): - # Expected validation errors from malformed input - pass + 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: + 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)]) + params.to_bytes() atheris.Setup(sys.argv, TestOneInput) diff --git a/.clusterfuzzlite/entity_id_fuzzer.py b/.clusterfuzzlite/entity_id_fuzzer.py index 87bf8b2d2..fdbbc509f 100644 --- a/.clusterfuzzlite/entity_id_fuzzer.py +++ b/.clusterfuzzlite/entity_id_fuzzer.py @@ -16,7 +16,8 @@ def _try_parse_entity_id(entity_id_class, text: str) -> None: try: entity_id_class.from_string(text) - except (TypeError, ValueError): + except (TypeError, ValueError, IndexError, OverflowError): + # Expected errors from malformed string input (parsing, indexing, conversions) pass diff --git a/.clusterfuzzlite/transaction_from_bytes_fuzzer.py b/.clusterfuzzlite/transaction_from_bytes_fuzzer.py index d2ca4d75b..c86bec7ba 100644 --- a/.clusterfuzzlite/transaction_from_bytes_fuzzer.py +++ b/.clusterfuzzlite/transaction_from_bytes_fuzzer.py @@ -16,9 +16,10 @@ def TestOneInput(data: bytes) -> None: try: tx = Transaction.from_bytes(data) tx.to_bytes() - except ValueError: - # All parsing / validation failures are expected; only unhandled - # exceptions that escape this function are reported as fuzzer crashes. + 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 From 9ad6e290117c8e183c52eaa5991fc10da15d08bb Mon Sep 17 00:00:00 2001 From: Ntege Daniel Date: Thu, 30 Apr 2026 21:03:18 +0300 Subject: [PATCH 11/11] fix: specify exception types for validation errors in TestOneInput Signed-off-by: Ntege Daniel --- .clusterfuzzlite/contract_params_fuzzer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.clusterfuzzlite/contract_params_fuzzer.py b/.clusterfuzzlite/contract_params_fuzzer.py index 73d9b29c3..f859ed95e 100644 --- a/.clusterfuzzlite/contract_params_fuzzer.py +++ b/.clusterfuzzlite/contract_params_fuzzer.py @@ -8,6 +8,8 @@ with atheris.instrument_imports(): + from eth_abi.exceptions import EncodingTypeError, ValueOutOfBounds + from hiero_sdk_python import ContractFunctionParameters @@ -42,7 +44,11 @@ def TestOneInput(data: bytes) -> None: else: count = fdp.ConsumeIntInRange(0, 8) params.add_bytes32_array([fdp.ConsumeBytes(32) for _ in range(count)]) - params.to_bytes() + 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)