Skip to content

Commit d4d0385

Browse files
committed
Add ClusterFuzzLite integration
Signed-off-by: Somesh Mal <malsomesh9@gmail.com>
1 parent 15032e1 commit d4d0385

8 files changed

Lines changed: 339 additions & 0 deletions

File tree

.clusterfuzzlite/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM gcr.io/oss-fuzz-base/base-builder-python
2+
3+
COPY . $SRC/hiero-sdk-python
4+
WORKDIR $SRC/hiero-sdk-python
5+
COPY .clusterfuzzlite/build.sh $SRC/build.sh

.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+
PROJECT_DIR="$SRC/hiero-sdk-python"
4+
5+
cd "$PROJECT_DIR"
6+
7+
python3 -m pip install --upgrade pip
8+
pip3 install grpcio-tools
9+
python3 generate_proto.py
10+
pip3 install .
11+
pip3 install atheris pyinstaller
12+
13+
for fuzzer in "$PROJECT_DIR"/.clusterfuzzlite/*_fuzzer.py; do
14+
fuzzer_basename=$(basename -s .py "$fuzzer")
15+
fuzzer_package="${fuzzer_basename}.pkg"
16+
17+
pyinstaller \
18+
--distpath "$OUT" \
19+
--workpath /tmp/pyinstaller-work \
20+
--specpath /tmp/pyinstaller-spec \
21+
--onefile \
22+
--name "$fuzzer_package" \
23+
"$fuzzer"
24+
25+
cat > "$OUT/$fuzzer_basename" <<EOF
26+
#!/bin/sh
27+
# LLVMFuzzerTestOneInput for fuzzer detection.
28+
this_dir=\$(dirname "\$0")
29+
"\$this_dir/$fuzzer_package" "\$@"
30+
EOF
31+
chmod +x "$OUT/$fuzzer_basename"
32+
done
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
5+
import atheris
6+
from eth_abi.exceptions import EncodingError, ParseError
7+
8+
9+
with atheris.instrument_imports():
10+
from hiero_sdk_python import ContractFunctionParameters
11+
12+
13+
EXPECTED_EXCEPTIONS = (EncodingError, OverflowError, ParseError, TypeError, ValueError)
14+
15+
16+
def _consume_function_name(provider: atheris.FuzzedDataProvider) -> str | None:
17+
if provider.ConsumeBool():
18+
return None
19+
20+
return provider.ConsumeUnicodeNoSurrogates(64)
21+
22+
23+
def _add_parameter(provider: atheris.FuzzedDataProvider, params: ContractFunctionParameters) -> None:
24+
choice = provider.ConsumeIntInRange(0, 7)
25+
26+
if choice == 0:
27+
params.add_bool(provider.ConsumeBool())
28+
elif choice == 1:
29+
params.add_address(provider.ConsumeBytes(20))
30+
elif choice == 2:
31+
params.add_address(provider.ConsumeBytes(20).hex())
32+
elif choice == 3:
33+
params.add_string(provider.ConsumeUnicodeNoSurrogates(256))
34+
elif choice == 4:
35+
params.add_bytes(provider.ConsumeBytes(256))
36+
elif choice == 5:
37+
params.add_bytes32(provider.ConsumeBytes(64))
38+
elif choice == 6:
39+
size = provider.ConsumeIntInRange(1, 32) * 8
40+
getattr(params, f"add_int{size}")(provider.ConsumeInt(256))
41+
else:
42+
size = provider.ConsumeIntInRange(1, 32) * 8
43+
getattr(params, f"add_uint{size}")(provider.ConsumeInt(256))
44+
45+
46+
def TestOneInput(data: bytes) -> None:
47+
provider = atheris.FuzzedDataProvider(data)
48+
params = ContractFunctionParameters(_consume_function_name(provider))
49+
50+
for _ in range(provider.ConsumeIntInRange(0, 8)):
51+
_add_parameter(provider, params)
52+
53+
try:
54+
params.to_bytes()
55+
except EXPECTED_EXCEPTIONS:
56+
return
57+
58+
59+
def main() -> None:
60+
atheris.Setup(sys.argv, TestOneInput)
61+
atheris.Fuzz()
62+
63+
64+
if __name__ == "__main__":
65+
main()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
import warnings
5+
6+
import atheris
7+
8+
9+
with atheris.instrument_imports():
10+
from hiero_sdk_python import AccountId, TokenId
11+
from hiero_sdk_python.contract.contract_id import ContractId
12+
13+
14+
EXPECTED_EXCEPTIONS = (TypeError, UnicodeDecodeError, ValueError)
15+
PARSERS = (AccountId.from_string, TokenId.from_string, ContractId.from_string)
16+
17+
18+
def _try_parse(parser, candidate: str) -> None:
19+
try:
20+
parser(candidate)
21+
except EXPECTED_EXCEPTIONS:
22+
return
23+
24+
25+
def _consume_text(provider: atheris.FuzzedDataProvider) -> str:
26+
return provider.ConsumeUnicodeNoSurrogates(256)
27+
28+
29+
def _candidate_inputs(provider: atheris.FuzzedDataProvider) -> list[str]:
30+
text = _consume_text(provider)
31+
shard = provider.ConsumeIntInRange(-10, 10)
32+
realm = provider.ConsumeIntInRange(-10, 10)
33+
number = provider.ConsumeIntInRange(-(2**31), 2**31 - 1)
34+
hex_text = provider.ConsumeBytes(48).hex()
35+
36+
return [
37+
text,
38+
f"{shard}.{realm}.{number}",
39+
f"{shard}.{realm}.{hex_text}",
40+
f"0x{hex_text}",
41+
]
42+
43+
44+
def TestOneInput(data: bytes) -> None:
45+
provider = atheris.FuzzedDataProvider(data)
46+
47+
with warnings.catch_warnings():
48+
warnings.simplefilter("ignore", UserWarning)
49+
for candidate in _candidate_inputs(provider):
50+
for parser in PARSERS:
51+
_try_parse(parser, candidate)
52+
53+
54+
def main() -> None:
55+
atheris.Setup(sys.argv, TestOneInput)
56+
atheris.Fuzz()
57+
58+
59+
if __name__ == "__main__":
60+
main()

.clusterfuzzlite/keys_fuzzer.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
import warnings
5+
6+
import atheris
7+
from cryptography.exceptions import UnsupportedAlgorithm
8+
9+
10+
with atheris.instrument_imports():
11+
from hiero_sdk_python import PrivateKey, PublicKey
12+
13+
14+
EXPECTED_EXCEPTIONS = (TypeError, UnsupportedAlgorithm, ValueError)
15+
BYTE_PARSERS = (PrivateKey.from_bytes, PublicKey.from_bytes)
16+
STRING_PARSERS = (PrivateKey.from_string, PublicKey.from_string)
17+
18+
19+
def _try_parse(parser, value: bytes | str) -> None:
20+
try:
21+
parser(value)
22+
except EXPECTED_EXCEPTIONS:
23+
return
24+
25+
26+
def TestOneInput(data: bytes) -> None:
27+
provider = atheris.FuzzedDataProvider(data)
28+
raw_bytes = provider.ConsumeBytes(512)
29+
raw_text = provider.ConsumeUnicodeNoSurrogates(1024)
30+
hex_text = raw_bytes.hex()
31+
32+
with warnings.catch_warnings():
33+
warnings.simplefilter("ignore", UserWarning)
34+
35+
for parser in BYTE_PARSERS:
36+
_try_parse(parser, raw_bytes)
37+
38+
for candidate in (raw_text, hex_text, f"0x{hex_text}"):
39+
for parser in STRING_PARSERS:
40+
_try_parse(parser, candidate)
41+
42+
43+
def main() -> None:
44+
atheris.Setup(sys.argv, TestOneInput)
45+
atheris.Fuzz()
46+
47+
48+
if __name__ == "__main__":
49+
main()

.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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
5+
import atheris
6+
from google.protobuf.message import DecodeError
7+
8+
9+
with atheris.instrument_imports():
10+
from hiero_sdk_python import Transaction
11+
12+
13+
EXPECTED_EXCEPTIONS = (DecodeError, TypeError, ValueError)
14+
15+
16+
def TestOneInput(data: bytes) -> None:
17+
try:
18+
Transaction.from_bytes(data)
19+
except EXPECTED_EXCEPTIONS:
20+
return
21+
22+
23+
def main() -> None:
24+
atheris.Setup(sys.argv, TestOneInput)
25+
atheris.Fuzz()
26+
27+
28+
if __name__ == "__main__":
29+
main()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: ClusterFuzzLite PR Fuzzing
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- ".clusterfuzzlite/**"
7+
- ".github/workflows/clusterfuzzlite.yml"
8+
- "generate_proto.py"
9+
- "pyproject.toml"
10+
- "src/**"
11+
- "tests/fuzz/**"
12+
push:
13+
branches:
14+
- "main"
15+
paths:
16+
- ".clusterfuzzlite/**"
17+
- ".github/workflows/clusterfuzzlite.yml"
18+
- "generate_proto.py"
19+
- "pyproject.toml"
20+
- "src/**"
21+
- "tests/fuzz/**"
22+
workflow_dispatch: {}
23+
24+
permissions:
25+
contents: read
26+
security-events: write
27+
28+
concurrency:
29+
group: clusterfuzzlite-${{ github.event.pull_request.number || github.ref }}
30+
cancel-in-progress: true
31+
32+
jobs:
33+
pr-fuzzing:
34+
name: PR Fuzzing (${{ matrix.sanitizer }})
35+
if: github.event_name == 'pull_request'
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 30
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
sanitizer:
42+
- address
43+
44+
steps:
45+
- name: Harden the runner (Audit all outbound calls)
46+
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
47+
with:
48+
egress-policy: audit
49+
50+
- name: Checkout repository
51+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
52+
53+
- name: Build fuzzers (${{ matrix.sanitizer }})
54+
id: build
55+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
56+
with:
57+
language: python
58+
github-token: ${{ secrets.GITHUB_TOKEN }}
59+
sanitizer: ${{ matrix.sanitizer }}
60+
61+
- name: Run fuzzers (${{ matrix.sanitizer }})
62+
id: run
63+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
64+
with:
65+
github-token: ${{ secrets.GITHUB_TOKEN }}
66+
fuzz-seconds: 600
67+
mode: code-change
68+
sanitizer: ${{ matrix.sanitizer }}
69+
output-sarif: true
70+
71+
continuous-build:
72+
name: Continuous Fuzzer Build (${{ matrix.sanitizer }})
73+
if: github.event_name == 'push'
74+
runs-on: ubuntu-latest
75+
timeout-minutes: 30
76+
strategy:
77+
fail-fast: false
78+
matrix:
79+
sanitizer:
80+
- address
81+
82+
steps:
83+
- name: Harden the runner (Audit all outbound calls)
84+
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
85+
with:
86+
egress-policy: audit
87+
88+
- name: Checkout repository
89+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
90+
91+
- name: Build fuzzers (${{ matrix.sanitizer }})
92+
id: build
93+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
94+
with:
95+
language: python
96+
github-token: ${{ secrets.GITHUB_TOKEN }}
97+
sanitizer: ${{ matrix.sanitizer }}
98+
upload-build: true

0 commit comments

Comments
 (0)