Skip to content

Commit 64370f1

Browse files
danielmarvNssGourav
authored andcommitted
feat: add fuzzing support with Atheris and Docker configuration (hiero-ledger#2177)
Signed-off-by: Ntege Daniel <danientege785@gmail.com>
1 parent 2964324 commit 64370f1

10 files changed

Lines changed: 284 additions & 0 deletions

File tree

.clusterfuzzlite/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM gcr.io/oss-fuzz-base/base-builder-python
2+
3+
RUN apt-get update && apt-get install -y --no-install-recommends \
4+
protobuf-compiler \
5+
&& rm -rf /var/lib/apt/lists/*
6+
7+
COPY . $SRC/hiero-sdk-python
8+
WORKDIR $SRC/hiero-sdk-python
9+
COPY .clusterfuzzlite/build.sh $SRC/

.clusterfuzzlite/build.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash -eu
2+
3+
pip3 install grpcio-tools
4+
5+
python3 generate_proto.py
6+
7+
pip3 install .
8+
9+
pip3 install atheris pyinstaller
10+
11+
find "$SRC/hiero-sdk-python/.clusterfuzzlite" -name '*_fuzzer.py' -print0 | while IFS= read -r -d '' fuzzer; do
12+
fuzzer_basename=$(basename -s .py "$fuzzer")
13+
fuzzer_package="${fuzzer_basename}.pkg"
14+
15+
# Bundle the fuzzer and all its dependencies into a single portable package.
16+
pyinstaller --distpath "$OUT" --onefile --name "$fuzzer_package" "$fuzzer"
17+
18+
# Write a thin shell wrapper that ClusterFuzzLite uses to invoke the fuzzer.
19+
# The wrapper preloads the sanitizer library and sets ASAN_OPTIONS.
20+
# LD_PRELOAD is required here because the SDK uses C extensions (grpcio,
21+
# cryptography, protobuf) that must be covered by the sanitizer.
22+
cat > "$OUT/$fuzzer_basename" << EOF
23+
#!/bin/sh
24+
# LLVMFuzzerTestOneInput for fuzzer detection.
25+
this_dir=\$(dirname "\$0")
26+
LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \\
27+
PYCRYPTODOME_DISABLE_DEEPBIND=1 \\
28+
ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \\
29+
"\$this_dir/$fuzzer_package" "\$@"
30+
EOF
31+
chmod +x "$OUT/$fuzzer_basename"
32+
done
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Atheris fuzz target: ContractFunctionParameters ABI encoding."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import atheris
8+
9+
10+
with atheris.instrument_imports():
11+
from eth_abi.exceptions import EncodingTypeError, ValueOutOfBounds
12+
13+
from hiero_sdk_python import ContractFunctionParameters
14+
15+
16+
def TestOneInput(data: bytes) -> None:
17+
"""Feed arbitrary bytes into ContractFunctionParameters encoding paths."""
18+
fdp = atheris.FuzzedDataProvider(data)
19+
choice = fdp.ConsumeIntInRange(0, 9)
20+
params = ContractFunctionParameters()
21+
if choice == 0:
22+
params.add_bool(fdp.ConsumeBool())
23+
elif choice == 1:
24+
# add_address expects a 20-byte hex string or bytes
25+
params.add_address(fdp.ConsumeBytes(20).hex())
26+
elif choice == 2:
27+
params.add_string(fdp.ConsumeUnicodeNoSurrogates(256))
28+
elif choice == 3:
29+
params.add_bytes(fdp.ConsumeBytes(256))
30+
elif choice == 4:
31+
params.add_bytes32(fdp.ConsumeBytes(32))
32+
elif choice == 5:
33+
count = fdp.ConsumeIntInRange(0, 8)
34+
params.add_bool_array([fdp.ConsumeBool() for _ in range(count)])
35+
elif choice == 6:
36+
count = fdp.ConsumeIntInRange(0, 8)
37+
params.add_string_array([fdp.ConsumeUnicodeNoSurrogates(64) for _ in range(count)])
38+
elif choice == 7:
39+
count = fdp.ConsumeIntInRange(0, 8)
40+
params.add_bytes_array([fdp.ConsumeBytes(32) for _ in range(count)])
41+
elif choice == 8:
42+
count = fdp.ConsumeIntInRange(0, 8)
43+
params.add_address_array([fdp.ConsumeBytes(20).hex() for _ in range(count)])
44+
else:
45+
count = fdp.ConsumeIntInRange(0, 8)
46+
params.add_bytes32_array([fdp.ConsumeBytes(32) for _ in range(count)])
47+
try:
48+
params.to_bytes()
49+
except (TypeError, ValueError, EncodingTypeError, ValueOutOfBounds):
50+
# Malformed fuzz input can trigger expected validation/ABI encoding failures.
51+
pass
52+
53+
54+
atheris.Setup(sys.argv, TestOneInput)
55+
atheris.Fuzz()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Atheris fuzz target: entity ID string parsing."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import atheris
8+
9+
10+
with atheris.instrument_imports():
11+
from hiero_sdk_python import AccountId, ContractId, FileId, TokenId, TopicId
12+
13+
_CLASSES = (AccountId, TokenId, ContractId, FileId, TopicId)
14+
15+
16+
def _try_parse_entity_id(entity_id_class, text: str) -> None:
17+
try:
18+
entity_id_class.from_string(text)
19+
except (TypeError, ValueError, IndexError, OverflowError):
20+
# Expected errors from malformed string input (parsing, indexing, conversions)
21+
pass
22+
23+
24+
def TestOneInput(data: bytes) -> None:
25+
"""Feed arbitrary strings into entity ID parsers."""
26+
fdp = atheris.FuzzedDataProvider(data)
27+
text = fdp.ConsumeUnicodeNoSurrogates(256)
28+
29+
for cls in _CLASSES:
30+
_try_parse_entity_id(cls, text)
31+
32+
33+
atheris.Setup(sys.argv, TestOneInput)
34+
atheris.Fuzz()

.clusterfuzzlite/keys_fuzzer.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Atheris fuzz target: PrivateKey and PublicKey parsing."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
import warnings
7+
8+
import atheris
9+
10+
11+
with atheris.instrument_imports():
12+
from hiero_sdk_python import PrivateKey, PublicKey
13+
14+
15+
def _quiet(fn, *args):
16+
"""Call *fn* with *args*, suppressing UserWarning."""
17+
with warnings.catch_warnings():
18+
warnings.simplefilter("ignore", UserWarning)
19+
return fn(*args)
20+
21+
22+
def TestOneInput(data: bytes) -> None:
23+
"""Feed arbitrary bytes/strings into key parsers."""
24+
fdp = atheris.FuzzedDataProvider(data)
25+
choice = fdp.ConsumeIntInRange(0, 3)
26+
27+
try:
28+
if choice == 0:
29+
text = fdp.ConsumeUnicodeNoSurrogates(256)
30+
_quiet(PrivateKey.from_string, text)
31+
elif choice == 1:
32+
raw = fdp.ConsumeBytes(128)
33+
_quiet(PrivateKey.from_bytes, raw)
34+
elif choice == 2:
35+
text = fdp.ConsumeUnicodeNoSurrogates(256)
36+
_quiet(PublicKey.from_string, text)
37+
else:
38+
raw = fdp.ConsumeBytes(128)
39+
_quiet(PublicKey.from_bytes, raw)
40+
except ValueError:
41+
pass
42+
43+
44+
atheris.Setup(sys.argv, TestOneInput)
45+
atheris.Fuzz()

.clusterfuzzlite/project.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
language: python
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Atheris fuzz target: Transaction.from_bytes deserialization."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import atheris
8+
9+
10+
with atheris.instrument_imports():
11+
from hiero_sdk_python import Transaction
12+
13+
14+
def TestOneInput(data: bytes) -> None:
15+
"""Feed arbitrary bytes into Transaction.from_bytes and re-serialise."""
16+
try:
17+
tx = Transaction.from_bytes(data)
18+
tx.to_bytes()
19+
except Exception:
20+
# Protobuf deserialization and transaction parsing can raise many
21+
# exception types (DecodeError, ValueError, TypeError, KeyError, etc.);
22+
# only unhandled exceptions that escape are reported as fuzzer crashes.
23+
pass
24+
25+
26+
atheris.Setup(sys.argv, TestOneInput)
27+
atheris.Fuzz()

.codacy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
exclude_paths:
33
- "tck/**"
4+
- ".clusterfuzzlite/**"
45

56
engines:
67
bandit:
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: ClusterFuzzLite
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
permissions: read-all
10+
11+
jobs:
12+
# ── PR fuzzing ────────────────────────────────────────────────────────────
13+
# Runs on every pull request and reports any new crashes found in the
14+
# changed code paths (code-change mode).
15+
PR:
16+
if: github.event_name == 'pull_request'
17+
runs-on: hl-sdk-py-lin-md
18+
concurrency:
19+
group: ${{ github.workflow }}-pr-${{ matrix.sanitizer }}-${{ github.ref }}
20+
cancel-in-progress: true
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
sanitizer:
25+
- address
26+
steps:
27+
- name: Harden the runner (Audit all outbound calls)
28+
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
29+
with:
30+
egress-policy: audit
31+
32+
- name: Build Fuzzers (${{ matrix.sanitizer }})
33+
id: build
34+
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
35+
with:
36+
language: python
37+
github-token: ${{ secrets.GITHUB_TOKEN }}
38+
sanitizer: ${{ matrix.sanitizer }}
39+
40+
- name: Run Fuzzers (${{ matrix.sanitizer }})
41+
id: run
42+
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
43+
with:
44+
github-token: ${{ secrets.GITHUB_TOKEN }}
45+
fuzz-seconds: 600
46+
mode: code-change
47+
sanitizer: ${{ matrix.sanitizer }}
48+
output-sarif: true
49+
50+
# ── Continuous builds ─────────────────────────────────────────────────────
51+
# Uploads a fresh fuzzer build artifact on every push to main so that PR
52+
# fuzzing can determine whether a crash was newly introduced.
53+
Build:
54+
if: github.event_name == 'push'
55+
runs-on: hl-sdk-py-lin-md
56+
concurrency:
57+
group: ${{ github.workflow }}-build-${{ matrix.sanitizer }}-${{ github.ref }}
58+
cancel-in-progress: true
59+
strategy:
60+
fail-fast: false
61+
matrix:
62+
sanitizer:
63+
- address
64+
steps:
65+
- name: Harden the runner (Audit all outbound calls)
66+
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
67+
with:
68+
egress-policy: audit
69+
70+
- name: Build Fuzzers (${{ matrix.sanitizer }})
71+
id: build
72+
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
73+
with:
74+
language: python
75+
sanitizer: ${{ matrix.sanitizer }}
76+
upload-build: true

src/hiero_sdk_python/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from .contract.contract_execute_transaction import ContractExecuteTransaction
3232
from .contract.contract_function_parameters import ContractFunctionParameters
3333
from .contract.contract_function_result import ContractFunctionResult
34+
from .contract.contract_id import ContractId
3435
from .contract.contract_info import ContractInfo
3536
from .contract.contract_info_query import ContractInfoQuery
3637
from .contract.contract_update_transaction import ContractUpdateTransaction
@@ -52,6 +53,7 @@
5253
from .file.file_contents_query import FileContentsQuery
5354
from .file.file_create_transaction import FileCreateTransaction
5455
from .file.file_delete_transaction import FileDeleteTransaction
56+
from .file.file_id import FileId
5557
from .file.file_info import FileInfo
5658
from .file.file_info_query import FileInfoQuery
5759
from .file.file_update_transaction import FileUpdateTransaction
@@ -249,6 +251,7 @@
249251
# File
250252
"FileCreateTransaction",
251253
"FileAppendTransaction",
254+
"FileId",
252255
"FileInfoQuery",
253256
"FileInfo",
254257
"FileContentsQuery",
@@ -257,6 +260,7 @@
257260
# Contract
258261
"ContractCreateTransaction",
259262
"ContractCallQuery",
263+
"ContractId",
260264
"ContractInfoQuery",
261265
"ContractBytecodeQuery",
262266
"ContractExecuteTransaction",

0 commit comments

Comments
 (0)