Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM gcr.io/oss-fuzz-base/base-builder-python@sha256:c1629d34bc645be7551e1ade467aff32ec2058875f2b27eb5471d964374d3d1e

COPY . $SRC/hiero-sdk-python
WORKDIR $SRC/hiero-sdk-python
COPY .clusterfuzzlite/build.sh $SRC/build.sh
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

PROJECT_DIR="$SRC/hiero-sdk-python"

cd "$PROJECT_DIR"

python3 -m pip install --upgrade pip
pip3 install grpcio-tools
python3 generate_proto.py
pip3 install .
pip3 install atheris pyinstaller

for fuzzer in "$PROJECT_DIR"/.clusterfuzzlite/*_fuzzer.py; do
fuzzer_basename=$(basename -s .py "$fuzzer")
fuzzer_package="${fuzzer_basename}.pkg"

pyinstaller \
--distpath "$OUT" \
--workpath /tmp/pyinstaller-work \
--specpath /tmp/pyinstaller-spec \
--onefile \
--name "$fuzzer_package" \
"$fuzzer"

cat > "$OUT/$fuzzer_basename" <<EOF
#!/bin/sh
# LLVMFuzzerTestOneInput for fuzzer detection.
this_dir=\$(dirname "\$0")
"\$this_dir/$fuzzer_package" "\$@"
EOF
chmod +x "$OUT/$fuzzer_basename"
done
65 changes: 65 additions & 0 deletions .clusterfuzzlite/contract_params_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from __future__ import annotations

import sys

import atheris
from eth_abi.exceptions import EncodingError, ParseError


with atheris.instrument_imports():
from hiero_sdk_python import ContractFunctionParameters


EXPECTED_EXCEPTIONS = (EncodingError, OverflowError, ParseError, TypeError, ValueError)


def _consume_function_name(provider: atheris.FuzzedDataProvider) -> str | None:
if provider.ConsumeBool():
return None

return provider.ConsumeUnicodeNoSurrogates(64)


def _add_parameter(provider: atheris.FuzzedDataProvider, params: ContractFunctionParameters) -> None:
choice = provider.ConsumeIntInRange(0, 7)

if choice == 0:
params.add_bool(provider.ConsumeBool())
elif choice == 1:
params.add_address(provider.ConsumeBytes(20))
elif choice == 2:
params.add_address(provider.ConsumeBytes(20).hex())
elif choice == 3:
params.add_string(provider.ConsumeUnicodeNoSurrogates(256))
elif choice == 4:
params.add_bytes(provider.ConsumeBytes(256))
elif choice == 5:
params.add_bytes32(provider.ConsumeBytes(64))
elif choice == 6:
size = provider.ConsumeIntInRange(1, 32) * 8
getattr(params, f"add_int{size}")(provider.ConsumeInt(256))
else:
size = provider.ConsumeIntInRange(1, 32) * 8
getattr(params, f"add_uint{size}")(provider.ConsumeInt(256))


def TestOneInput(data: bytes) -> None:
provider = atheris.FuzzedDataProvider(data)
params = ContractFunctionParameters(_consume_function_name(provider))

for _ in range(provider.ConsumeIntInRange(0, 8)):
_add_parameter(provider, params)

try:
params.to_bytes()
except EXPECTED_EXCEPTIONS:
return


def main() -> None:
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()


if __name__ == "__main__":
main()
60 changes: 60 additions & 0 deletions .clusterfuzzlite/entity_id_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

import sys
import warnings

import atheris


with atheris.instrument_imports():
from hiero_sdk_python import AccountId, TokenId
from hiero_sdk_python.contract.contract_id import ContractId


EXPECTED_EXCEPTIONS = (TypeError, UnicodeDecodeError, ValueError)
PARSERS = (AccountId.from_string, TokenId.from_string, ContractId.from_string)


def _try_parse(parser, candidate: str) -> None:
try:
parser(candidate)
except EXPECTED_EXCEPTIONS:
return


def _consume_text(provider: atheris.FuzzedDataProvider) -> str:
return provider.ConsumeUnicodeNoSurrogates(256)


def _candidate_inputs(provider: atheris.FuzzedDataProvider) -> list[str]:
text = _consume_text(provider)
shard = provider.ConsumeIntInRange(-10, 10)
realm = provider.ConsumeIntInRange(-10, 10)
number = provider.ConsumeIntInRange(-(2**31), 2**31 - 1)
hex_text = provider.ConsumeBytes(48).hex()

return [
text,
f"{shard}.{realm}.{number}",
f"{shard}.{realm}.{hex_text}",
f"0x{hex_text}",
]


def TestOneInput(data: bytes) -> None:
provider = atheris.FuzzedDataProvider(data)

with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
for candidate in _candidate_inputs(provider):
for parser in PARSERS:
_try_parse(parser, candidate)


def main() -> None:
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions .clusterfuzzlite/keys_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

import sys
import warnings

import atheris
from cryptography.exceptions import UnsupportedAlgorithm


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


EXPECTED_EXCEPTIONS = (TypeError, UnsupportedAlgorithm, ValueError)
BYTE_PARSERS = (PrivateKey.from_bytes, PublicKey.from_bytes)
STRING_PARSERS = (PrivateKey.from_string, PublicKey.from_string)


def _try_parse(parser, value: bytes | str) -> None:
try:
parser(value)
except EXPECTED_EXCEPTIONS:
return


def TestOneInput(data: bytes) -> None:
provider = atheris.FuzzedDataProvider(data)
raw_bytes = provider.ConsumeBytes(512)
raw_text = provider.ConsumeUnicodeNoSurrogates(1024)
hex_text = raw_bytes.hex()

with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)

for parser in BYTE_PARSERS:
_try_parse(parser, raw_bytes)

for candidate in (raw_text, hex_text, f"0x{hex_text}"):
for parser in STRING_PARSERS:
_try_parse(parser, candidate)


def main() -> None:
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions .clusterfuzzlite/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
language: python
29 changes: 29 additions & 0 deletions .clusterfuzzlite/transaction_from_bytes_fuzzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

import sys

import atheris
from google.protobuf.message import DecodeError


with atheris.instrument_imports():
from hiero_sdk_python import Transaction


EXPECTED_EXCEPTIONS = (DecodeError, TypeError, ValueError)


def TestOneInput(data: bytes) -> None:
try:
Transaction.from_bytes(data)
except EXPECTED_EXCEPTIONS:
return


def main() -> None:
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()


if __name__ == "__main__":
main()
98 changes: 98 additions & 0 deletions .github/workflows/clusterfuzzlite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: ClusterFuzzLite PR Fuzzing

on:
pull_request:
paths:
- ".clusterfuzzlite/**"
- ".github/workflows/clusterfuzzlite.yml"
- "generate_proto.py"
- "pyproject.toml"
- "src/**"
- "tests/fuzz/**"
push:
branches:
- "main"
paths:
- ".clusterfuzzlite/**"
- ".github/workflows/clusterfuzzlite.yml"
- "generate_proto.py"
- "pyproject.toml"
- "src/**"
- "tests/fuzz/**"
workflow_dispatch: {}

permissions:
contents: read
security-events: write

concurrency:
group: clusterfuzzlite-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
pr-fuzzing:
name: PR Fuzzing (${{ matrix.sanitizer }})
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
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: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Build fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@82652fb49e77bc29c35da1167bb286e93c6bcc05 # 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@82652fb49e77bc29c35da1167bb286e93c6bcc05 # v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600
mode: code-change
sanitizer: ${{ matrix.sanitizer }}
output-sarif: true

continuous-build:
name: Continuous Fuzzer Build (${{ matrix.sanitizer }})
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 30
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: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Build fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@82652fb49e77bc29c35da1167bb286e93c6bcc05 # v1
with:
language: python
github-token: ${{ secrets.GITHUB_TOKEN }}
sanitizer: ${{ matrix.sanitizer }}
upload-build: true