Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9e51dd7
feat(build): add multi-language fuzzing infra (CFLite + Codecov flags)
Apr 28, 2026
f3bc88c
fix(ci): exclude cargo-fuzz crates from clippy lint loop
Apr 28, 2026
41a71b4
fix(ci): emit valid {folderName:[]} JSON for fuzz outputs to prevent …
Apr 28, 2026
62ee68a
fix(fuzz): pin ClusterFuzzLite SHA and integrate fuzzing into pr-vali…
Apr 28, 2026
963f109
fix(workflows): grant actions: read to fuzz-pr detect-changes job
Apr 29, 2026
a86398f
chore(fuzz): trigger end-to-end CI validation for rust/python/js fuzz…
Apr 29, 2026
0bf0fd2
fix(workflows): lowercase fuzz boolean outputs and broaden JS fuzz regex
Apr 29, 2026
4e505a3
fix(workflows): correct Detect-Folder-Changes param names and pass Ba…
Apr 29, 2026
c2133f5
fix(ci): use hashtable splat for Detect-Folder-Changes args
Apr 30, 2026
e007f53
fix(fuzz): install python3.12 via deadsnakes PPA on focal base image
Apr 30, 2026
b4b6fb3
fix(fuzz): drop apt =* version pins blocking python3.12-dev install
Apr 30, 2026
b32fa1e
fix(fuzz): downgrade python 3.12 to 3.11 for atheris compat
Apr 30, 2026
ce80968
ci(fuzzing): swap base image to ubuntu-24-04 for working deadsnakes 3…
Apr 30, 2026
ca64043
fix(fuzz): install prebuild toolchain for jazzer.js source-build fall…
Apr 30, 2026
9cba651
fix(clusterfuzzlite): filter non-cargo-fuzz directories in build_rust.sh
Apr 30, 2026
7241a74
fix(fuzz): scope ClusterFuzzLite to Rust-only to fix GLIBC mismatch
Apr 30, 2026
bd5d11b
Merge remote-tracking branch 'origin/main' into feat/issue-150-fuzzin…
Apr 30, 2026
f24bbb9
fix(lint): guard PSScriptAnalyzer against internal NullReferenceExcep…
Apr 30, 2026
84f3ee7
feat(fuzz): add ClusterFuzzLite Python and JavaScript polyglot contai…
May 1, 2026
6511da6
feat(fuzz): add smoke harnesses and hadolint waiver to trigger Cluste…
May 1, 2026
643be68
Merge remote-tracking branch 'origin/main' into feat/issue-150-fuzzin…
May 1, 2026
e1ddc05
Merge remote-tracking branch 'origin/main' into feat/issue-150-fuzzin…
May 1, 2026
56088a8
fix(ci): set python/js sanitizer none, install atheris, add cspell wo…
May 1, 2026
25b07d2
fix(docs): format markdown tables in clusterfuzzlite README
May 1, 2026
81465ff
fix(ci): use address sanitizer for js/python fuzz jobs (none invalid)
May 2, 2026
9da00c5
fix(fuzz): pre-install atheris in CFL image and revert JS sanitizer t…
May 2, 2026
f729966
fix(fuzz): drop polyglot ARG pattern and switch JS sanitizer to coverage
May 3, 2026
4485fe2
fix(fuzz): dispatch on FUZZING_LANGUAGE so python/js jobs skip rust h…
May 3, 2026
6e66188
fix(fuzz): install Node 20 in CFLite image; conditional --collect-sub…
May 4, 2026
d329b30
fix(fuzz): install Node 20 via prebuilt tarball to avoid Ubuntu mirro…
May 4, 2026
0146663
fix(fuzz): relax npm install and add libFuzzer marker for bad_build_c…
May 4, 2026
3c85131
fix(fuzz): correct ConnectorClient class name and bundle src.message_…
May 6, 2026
3a77bd4
Merge remote-tracking branch 'origin/main' into feat/issue-150-fuzzin…
May 6, 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
52 changes: 52 additions & 0 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ClusterFuzzLite builder image for edge-ai fuzz harnesses.
#
# Single-Dockerfile, single-base-image pattern (issue #459): the CFLite GitHub
# Action does NOT forward its `language:` input as a Docker build-arg, so a
# `LANGUAGE`-parameterized base image cannot be selected at build time. Instead
# we build on top of `base-builder-rust` (which contains the OSS-Fuzz tooling
# plus a working python3) and dispatch to per-language `build_<lang>.sh`
# scripts via the shared `build.sh`. The OSS-Fuzz `compile` step uses the
# runtime `FUZZING_LANGUAGE` env var (set by CFLite from `language:`) to know
# how to wrap each harness.
#
# IMPORTANT: do NOT pin a base-image tag (e.g. :ubuntu-24-04). The
# ClusterFuzzLite GitHub Action runner is Ubuntu 20.04 (glibc 2.31). The
# 24.04-tagged base produces binaries linked against glibc >= 2.32 which fail
# bad_build_check on the runner ("GLIBC_2.32 not found"). The default tag
# tracks the runner's glibc.
FROM gcr.io/oss-fuzz-base/base-builder-rust

ENV DEBIAN_FRONTEND=noninteractive

# Node.js (and npm) are required by build_js.sh to run `npm ci` and `npx jazzer`
# for JavaScript fuzz harnesses. The base-builder-rust image does not include
# Node. Install Node 20.x from the official prebuilt linux-x64 tarball on
# nodejs.org instead of apt: the CFLite build container cannot reliably reach
# Ubuntu's archive/security mirrors (port 80 to archive.ubuntu.com and
# security.ubuntu.com regularly times out from the runner), but HTTPS to
# nodejs.org works. curl + ca-certificates are already present in the base
# image.
ARG NODE_VERSION=20.18.1
# hadolint ignore=DL3003
RUN curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \
&& tar -xJf "node-v${NODE_VERSION}-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v${NODE_VERSION}-linux-x64.tar.xz" \
&& node --version \
&& npm --version

# atheris must be present in the image before the OSS-Fuzz `compile` step runs
# (compile imports the Python fuzz targets to wrap them, which fails if atheris
# is not yet installed). Install unconditionally: the ClusterFuzzLite GitHub
# Action does not forward its `language:` input as a Docker build-arg, so the
# `LANGUAGE` ARG above is always the default at image-build time and a
# conditional install would never fire for python harnesses.
# hadolint ignore=DL3013
RUN python3 -m pip install --no-cache-dir atheris pyinstaller

COPY . $SRC/edge-ai
COPY .clusterfuzzlite/build.sh $SRC/build.sh
COPY .clusterfuzzlite/build_rust.sh $SRC/build_rust.sh
COPY .clusterfuzzlite/build_python.sh $SRC/build_python.sh
COPY .clusterfuzzlite/build_js.sh $SRC/build_js.sh

WORKDIR $SRC/edge-ai
72 changes: 72 additions & 0 deletions .clusterfuzzlite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# ClusterFuzzLite Builder Containers

This directory configures the [ClusterFuzzLite][cflite] (CFLite) builder
containers and per-language fuzz harness build scripts for `edge-ai`.

## Architecture

CFLite expects a single `Dockerfile` and a single `build.sh` at the project
root (here, `.clusterfuzzlite/`). The `build_fuzzers` GitHub Action does not
expose a `dockerfile-path` input, so all three languages share one Dockerfile
parameterized by an `ARG LANGUAGE` build-arg that the action forwards from its
`language:` workflow input.

* `Dockerfile` — selects `gcr.io/oss-fuzz-base/base-builder-${LANGUAGE}` at
build time. Default `LANGUAGE=rust` preserves the historical behavior.
* `build.sh` — top-level dispatcher. Inspects the `LANGUAGE` env var (also set
by the action) and execs the matching `build_<lang>.sh`.
* `build_rust.sh` / `build_python.sh` / `build_js.sh` — per-language harness
builders.

The base image tag is intentionally unpinned: the CFLite Action runner is
Ubuntu 20.04 (glibc 2.31), and pinning a newer tag (e.g. `:ubuntu-24-04`)
produces binaries linked against glibc >= 2.32 that fail `bad_build_check`
on the runner.

## Language toolchains

| Language | Engine | Build pattern |
|--------------|------------|------------------------------------------------------|
| `rust` | cargo-fuzz | `cargo +nightly fuzz build` per harness, copy to OUT |
| `python` | Atheris | `pyinstaller --onefile` + ASAN-aware bash wrapper |
| `javascript` | Jazzer.js | `npm ci` in the harness service + `npx jazzer` shim |

## Adding a Python harness

1. Place the harness at `tests/fuzz/fuzz_<name>.py` inside the target service.
2. Append an entry to the `HARNESSES` array in [`build_python.sh`](./build_python.sh)
in the form `<harness_name>:<service_dir>:<harness_rel_path>`.
3. Ensure the service ships a `requirements.txt` so transitive deps are
installed before `pyinstaller` runs.
4. If a new top-level component number is introduced, add a matching
`fuzz-py-<NNN>` flag to [`codecov.yml`](../codecov.yml).

## Adding a JavaScript harness

1. Place the harness at `tests/fuzz/fuzz_<name>.mjs` inside the target service.
2. Append an entry to the `HARNESSES` array in [`build_js.sh`](./build_js.sh).
3. Ensure the service has a committed `package-lock.json` so `npm ci` succeeds
reproducibly.
4. Declare `@jazzer.js/core` in the service's `devDependencies` for local
repro and editor IntelliSense (CFLite preinstalls it globally in the base
image, but the manifest entry keeps repos buildable outside CFLite).

## Lint waivers

* `Dockerfile` carries a `# hadolint ignore=DL3006` directive on the
`FROM gcr.io/oss-fuzz-base/base-builder-${LANGUAGE}` line. The base image
tag is intentionally unpinned (see Architecture above); pinning to a
specific Ubuntu release breaks `bad_build_check` on the CFLite runner.

## Known limitations

* The `506-ros2-connector` harness fuzzes the in-process message registry
only (paho-mqtt + pure-Python typed accessors). Fuzzing the full ROS 2
bridge is out of scope because `rclpy` is not installable from PyPI; that
work would require a derived base image.
* Atheris 3.0.0 pins Python 3.11. Upgrading the CFLite base image to Ubuntu
24.04 (issue [#454][i454]) cannot proceed without an Atheris 3.12 wheel or
a replacement Python fuzzing engine.

[cflite]: https://google.github.io/clusterfuzzlite/
[i454]: https://github.com/microsoft/edge-ai/issues/454
19 changes: 19 additions & 0 deletions .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# ClusterFuzzLite top-level build dispatcher. Selects the per-language
# builder based on the OSS-Fuzz canonical `FUZZING_LANGUAGE` env var, which
# is set by the CFLite action's `language:` input on the inner `compile`
# container. `LANGUAGE` is accepted as a fallback for local repro convenience
# and defaults to rust to preserve historical behavior when neither is set.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

case "${FUZZING_LANGUAGE:-${LANGUAGE:-rust}}" in
rust) bash "${SCRIPT_DIR}/build_rust.sh" ;;
python) bash "${SCRIPT_DIR}/build_python.sh" ;;
javascript) bash "${SCRIPT_DIR}/build_js.sh" ;;
*)
echo "build.sh: unsupported FUZZING_LANGUAGE='${FUZZING_LANGUAGE:-${LANGUAGE:-}}' (expected rust|python|javascript)" >&2
exit 1
;;
esac
41 changes: 41 additions & 0 deletions .clusterfuzzlite/build_js.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Build JavaScript Jazzer.js fuzz harnesses. The base-builder-javascript image
# preinstalls @jazzer.js/core globally; we additionally install the harness
# service's npm dependencies so any local imports resolve at runtime.
set -euo pipefail

: "${OUT:?OUT must be set by ClusterFuzzLite}"
: "${SRC:?SRC must be set by ClusterFuzzLite}"

# Format: "<harness_name>:<absolute path to harness .mjs>"
HARNESSES=(
"fuzz_processAlerts_513:$SRC/edge-ai/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_processAlerts.mjs"
"fuzz_smoke_513:$SRC/edge-ai/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_smoke.mjs"
)

if [[ ${#HARNESSES[@]} -eq 0 ]]; then
echo "build_js.sh: no JavaScript fuzz harnesses configured"
exit 0
fi

# Install npm dependencies for harness service(s) so local module resolution
# works inside the wrapper at fuzz time.
pushd "${SRC}/edge-ai/src/500-application/513-tiered-notification-service" >/dev/null
npm install --no-audit --no-fund
popd >/dev/null

for entry in "${HARNESSES[@]}"; do
harness_name="${entry%%:*}"
harness_path="${entry#*:}"
out_path="${OUT}/${harness_name}"
svc_dir="$(dirname "$(dirname "$(dirname "${harness_path}")")")"

# Run the harness in place so its relative imports (e.g. ../../src/...)
# resolve against the original service tree instead of a flattened OUT dir.
cat >"${out_path}" <<WRAPPER
#!/usr/bin/env bash
cd "${svc_dir}"
exec npx jazzer "${harness_path}" "\$@"
WRAPPER
chmod +x "${out_path}"
done
74 changes: 74 additions & 0 deletions .clusterfuzzlite/build_python.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Build Python Atheris fuzz harnesses using the CFLite-canonical PyInstaller
# `--onefile` convention: one self-contained executable per harness, plus a
# small wrapper that sets ASAN options expected by the fuzzing engine.
set -euo pipefail

: "${OUT:?OUT must be set by ClusterFuzzLite}"
: "${SRC:?SRC must be set by ClusterFuzzLite}"

# Format: "<harness_name>:<service dir relative to repo root>:<harness path relative to service dir>"
HARNESSES=(
"fuzz_models_505:src/500-application/505-akri-rest-http-connector/services/sensor-simulator:tests/fuzz/fuzz_models.py"
"fuzz_message_registry_506:src/500-application/506-ros2-connector/services/ros2-connector:tests/fuzz/fuzz_message_registry.py"
"fuzz_process_event_509:src/500-application/509-sse-connector/services/connector-test-client:tests/fuzz/fuzz_process_event.py"
"fuzz_soap_parser_510:src/500-application/510-onvif-connector/services/onvif-camera-simulator:tests/fuzz/fuzz_soap_parser.py"
"fuzz_smoke_505:src/500-application/505-akri-rest-http-connector/services/sensor-simulator:tests/fuzz/fuzz_smoke.py"
"fuzz_smoke_509:src/500-application/509-sse-connector/services/connector-test-client:tests/fuzz/fuzz_smoke.py"
"fuzz_smoke_510:src/500-application/510-onvif-connector/services/onvif-camera-simulator:tests/fuzz/fuzz_smoke.py"
)

if [[ ${#HARNESSES[@]} -eq 0 ]]; then
echo "build_python.sh: no Python fuzz harnesses configured"
exit 0
fi

# atheris and pyinstaller are pre-installed in the Dockerfile for the python
# language path so they are available before the OSS-Fuzz `compile` wrapper
# runs. Local-repro fallbacks below cover environments using a stock
# base-builder-python image.
if ! command -v pyinstaller >/dev/null 2>&1; then
python3 -m pip install --no-cache-dir pyinstaller
fi

if ! python3 -c "import atheris" >/dev/null 2>&1; then
python3 -m pip install --no-cache-dir atheris
fi

for entry in "${HARNESSES[@]}"; do
IFS=':' read -r harness_name svc_dir harness_rel <<<"${entry}"
svc_path="${SRC}/edge-ai/${svc_dir}"

pushd "${svc_path}" >/dev/null
if [[ -f requirements.txt ]]; then
pip3 install --no-cache-dir -r requirements.txt
fi
# Only collect src.message_types submodules for services that actually ship
# that package (currently 506-ros2-connector). PyInstaller aborts when asked
# to collect a non-existent package.
extra_args=()
if [[ -d "${svc_path}/src/message_types" ]]; then
extra_args+=(--collect-all src.message_types --add-data "${svc_path}/src/message_types:src/message_types")
fi
pyinstaller \
--distpath "${OUT}" \
--onefile \
--name "${harness_name}.pkg" \
--paths "${svc_path}" \
"${extra_args[@]}" \
"${svc_path}/${harness_rel}"
popd >/dev/null

# The `LLVMFuzzerTestOneInput` marker below is required: OSS-Fuzz's
# bad_build_check greps each target file for that string to recognize it as
# a libFuzzer-compatible fuzz target. Without it, the build is rejected with
# "No fuzz targets found in out dir."
cat >"${OUT}/${harness_name}" <<WRAPPER
#!/usr/bin/env bash
# LLVMFuzzerTestOneInput
this_dir=\$(dirname "\$0")
export ASAN_OPTIONS="\${ASAN_OPTIONS:-}:detect_leaks=0:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer"
exec "\$this_dir/${harness_name}.pkg" "\$@"
WRAPPER
chmod +x "${OUT}/${harness_name}"
done
55 changes: 55 additions & 0 deletions .clusterfuzzlite/build_rust.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Build all Rust cargo-fuzz harnesses discovered under src/ and copy artifacts to $OUT.
set -euo pipefail

: "${SRC:?SRC must be set by ClusterFuzzLite}"
: "${OUT:?OUT must be set by ClusterFuzzLite}"

REPO_ROOT="${SRC}/edge-ai"
RUST_TOOLCHAIN="nightly-2026-04-01"

shopt -s nullglob globstar

mapfile -t candidate_dirs < <(find "${REPO_ROOT}/src" -type d -name fuzz -not -path '*/target/*' | sort)

fuzz_dirs=()
for candidate in "${candidate_dirs[@]}"; do
parent_dir="$(dirname "${candidate}")"
if [[ -f "${candidate}/Cargo.toml" && -f "${parent_dir}/Cargo.toml" ]]; then
fuzz_dirs+=("${candidate}")
else
echo "build_rust.sh: skipping ${candidate} (not a cargo-fuzz harness layout)"
fi
done

if [[ ${#fuzz_dirs[@]} -eq 0 ]]; then
echo "build_rust.sh: no Rust fuzz harnesses discovered"
exit 0
fi

for fuzz_dir in "${fuzz_dirs[@]}"; do
crate_dir="$(dirname "${fuzz_dir}")"
crate_name="$(basename "${crate_dir}")"
echo "build_rust.sh: building harnesses in ${fuzz_dir}"

pushd "${crate_dir}" >/dev/null
cargo "+${RUST_TOOLCHAIN}" fuzz build -O
popd >/dev/null

target_dir="${crate_dir}/fuzz/target/x86_64-unknown-linux-gnu/release"
if [[ ! -d "${target_dir}" ]]; then
echo "build_rust.sh: expected build output ${target_dir} not found" >&2
exit 1
fi

for binary in "${target_dir}"/*; do
[[ -f "${binary}" && -x "${binary}" ]] || continue
harness_name="$(basename "${binary}")"
cp "${binary}" "${OUT}/${crate_name}_${harness_name}"

corpus_dir="${fuzz_dir}/corpus/${harness_name}"
if [[ -d "${corpus_dir}" ]]; then
(cd "${corpus_dir}" && zip -qr "${OUT}/${crate_name}_${harness_name}_seed_corpus.zip" .)
fi
done
done
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@
}
],
"dictionaries": ["k8s", "docker", "rust", "data-science", "aws", "terraform", "azure-services", "iot-operations", "microsoft-sample-companies", "industry-acronyms", "project-specific", "general-technical"],
"words": ["Acks", "analyzability", "behaviour", "cdylib", "Chronograf", "edgeai", "GHCP", "Kapacitor", "Kata", "Katas", "learning", "msazure", "myorg", "myorga", "myorgb", "SARIF", "Segoe", "shuf", "westeurope"]
"words": ["Acks", "analyzability", "ASAN", "Atheris", "atheris", "behaviour", "cdylib", "cflite", "Chronograf", "clusterfuzzlite", "distpath", "edgeai", "EUSAGE", "GHCP", "Kapacitor", "Kata", "Katas", "learning", "libfuzzer", "msazure", "myorg", "myorga", "myorgb", "onefile", "preinstalls", "pyinstaller", "SARIF", "Segoe", "shuf", "symbolizer", "westeurope"]
}
Loading
Loading