diff --git a/.github/actions/collect-fuzz-stats/action.yml b/.github/actions/collect-fuzz-stats/action.yml new file mode 100644 index 00000000..25487959 --- /dev/null +++ b/.github/actions/collect-fuzz-stats/action.yml @@ -0,0 +1,62 @@ +name: Collect fuzz stats +description: Dump coverage summaries (project-wide + per-harness) and corpus file counts into stats-/ for the report job. + +inputs: + project: + description: Project name. + required: true + variant: + description: Variant label — "baseline" or "current". + required: true + sha: + description: Commit SHA measured in this variant. + required: true + has_project: + description: String "true" if the project was present and built for this variant; otherwise only meta.json is written. + required: true + out_base: + description: Directory containing the oss-fuzz out tree (e.g. build/out for oss-fuzz template, /tmp/oss-fuzz/build/out for upstream). + required: true + +runs: + using: composite + steps: + - shell: bash + env: + PROJECT: ${{ inputs.project }} + VARIANT: ${{ inputs.variant }} + SHA: ${{ inputs.sha }} + HAS_PROJECT: ${{ inputs.has_project }} + OUT_BASE: ${{ inputs.out_base }} + run: | + OUT="stats-$VARIANT" + mkdir -p "$OUT/harness" + + jq -n \ + --arg variant "$VARIANT" \ + --arg sha "${SHA:-}" \ + --arg project "$PROJECT" \ + --arg has_project "${HAS_PROJECT:-false}" \ + '{variant: $variant, sha: $sha, project: $project, has_project: ($has_project == "true")}' \ + > "$OUT/meta.json" + + if [ "$HAS_PROJECT" != "true" ]; then + echo "No project at this variant; stats collection skipped" + exit 0 + fi + + PROJ_SUM=$(find "$OUT_BASE/$PROJECT/report/linux" -maxdepth 1 -name summary.json 2>/dev/null | head -1) + if [ -n "$PROJ_SUM" ]; then + cp "$PROJ_SUM" "$OUT/project.summary.json" + fi + + if [ -d "$OUT_BASE/$PROJECT/report_target" ]; then + for d in "$OUT_BASE/$PROJECT/report_target"/*/linux; do + [ -d "$d" ] || continue + HNAME=$(basename "$(dirname "$d")") + [ -f "$d/summary.json" ] && cp "$d/summary.json" "$OUT/harness/$HNAME.summary.json" + done + fi + + echo "---- $OUT ----" + find "$OUT" -type f -printf '%p (%s bytes)\n' diff --git a/.github/actions/invariance_tests/action.yml b/.github/actions/invariance_tests/action.yml deleted file mode 100644 index 3bc1c51d..00000000 --- a/.github/actions/invariance_tests/action.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Invariance tests -description: Run invariance tests and write summary. - -inputs: - artifact-name: - description: 'Name of the uploaded artifact' - required: false - default: 'invariance-test-results' - library-path: - description: 'Path to the InChI library under test' - required: false - default: 'CMake_build/full_build/INCHI-1-SRC/INCHI_API/libinchi/src/lib/libinchi.so' - -runs: - using: "composite" - steps: - - name: Run invariance tests - run: python INCHI-1-TEST/tests/test_library/inchi_tests/run_tests.py --test=invariance --lib-path=${{ inputs.library-path}} --data-config=INCHI-1-TEST/tests/test_library/config/config_ci.py - shell: bash - - - name: Write invariance summary - if: ${{ !cancelled() }} - run: python INCHI-1-TEST/tests/test_library/inchi_tests/parse_log.py --test=invariance --lib-path=${{ inputs.library-path}} --data-config=INCHI-1-TEST/tests/test_library/config/config_ci.py - shell: bash - - - name: Upload invariance test results - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: ${{ inputs.artifact-name }} - path: | - ./INCHI-1-TEST/tests/test_library/data/ci/*_invariance_ci.log - ./INCHI-1-TEST/tests/test_library/data/ci/*_invariance_ci_*.html diff --git a/.github/actions/post-fuzz-report/action.yml b/.github/actions/post-fuzz-report/action.yml new file mode 100644 index 00000000..7952e882 --- /dev/null +++ b/.github/actions/post-fuzz-report/action.yml @@ -0,0 +1,65 @@ +name: Post fuzz coverage report +description: Download baseline/current stats artifacts, render markdown, post or update a sticky PR comment. + +inputs: + footer: + description: Footer flavor — "generic" for oss-fuzz, "upstream" for upstream source PRs. + required: false + default: generic + fuzz_seconds: + description: Total fuzz budget used (for display only). + required: true + github_token: + description: Token with pull-requests:write to post the comment. + required: true + +runs: + using: composite + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: stats-baseline + path: stats/baseline + continue-on-error: true + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: stats-current + path: stats/current + continue-on-error: true + + - name: Render comment + shell: bash + env: + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + FUZZ_SECONDS: ${{ inputs.fuzz_seconds }} + FOOTER: ${{ inputs.footer }} + STATS_ROOT: stats + run: | + python3 "$GITHUB_ACTION_PATH/render_comment.py" > comment.md + echo "---- comment.md ----" + cat comment.md + + - name: Post or update PR comment + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + run: | + PR=$(gh pr list --repo "$REPO" --head "$BRANCH" --state open --json number --jq '.[0].number // empty') + if [ -z "$PR" ]; then + echo "No open PR for branch $BRANCH — skipping comment" + exit 0 + fi + MARKER='' + EXISTING=$(gh api "repos/$REPO/issues/$PR/comments" --paginate \ + --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" | head -1) + jq -Rs '{body: .}' < comment.md > payload.json + if [ -n "$EXISTING" ]; then + gh api --method PATCH "repos/$REPO/issues/comments/$EXISTING" --input payload.json > /dev/null + echo "Updated existing comment $EXISTING on PR #$PR" + else + gh api --method POST "repos/$REPO/issues/$PR/comments" --input payload.json > /dev/null + echo "Created new comment on PR #$PR" + fi diff --git a/.github/actions/post-fuzz-report/render_comment.py b/.github/actions/post-fuzz-report/render_comment.py new file mode 100644 index 00000000..a954f27c --- /dev/null +++ b/.github/actions/post-fuzz-report/render_comment.py @@ -0,0 +1,188 @@ +# Copyright 2026 fuzz-for-me contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Render before/after coverage comparison as a sticky PR comment body. + +Reads artifacts downloaded by the calling workflow: + stats/baseline/meta.json, project.summary.json, corpus.json, harness/*.summary.json + stats/current/ ... (same layout) + +Writes the markdown body to stdout. +""" + +import datetime +import json +import os +import pathlib +import sys + +MARKER = "" + +FOOTER_GENERIC = ( + "Per-harness data from `report_target/<fuzzer>/linux/summary.json`. " + "Full HTML reports in the workflow artifacts.") +FOOTER_UPSTREAM = ("Same harness config applied to both sides " + "(baseline = base source + PR harness). " + + FOOTER_GENERIC[5:]) + + +def _load_json(path: pathlib.Path): + if not path.exists(): + return None + try: + return json.loads(path.read_text()) + except json.JSONDecodeError: + return None + + +def _load_variant(base: pathlib.Path) -> dict: + harness: dict = {} + hd = base / "harness" + if hd.is_dir(): + for f in sorted(hd.glob("*.summary.json")): + data = _load_json(f) + if data is not None: + harness[f.name.removesuffix(".summary.json")] = data + return { + "meta": _load_json(base / "meta.json"), + "project": _load_json(base / "project.summary.json"), + "harness": harness, + } + + +def _totals(summary): + if not summary: + return None + t = summary["data"][0]["totals"] + return { + "lines": + (t["lines"]["covered"], t["lines"]["count"], t["lines"]["percent"]), + "branches": ( + t["branches"]["covered"], + t["branches"]["count"], + t["branches"]["percent"], + ), + "functions": ( + t["functions"]["covered"], + t["functions"]["count"], + t["functions"]["percent"], + ), + } + + +def _fmt_cov(tot, key): + if not tot: + return "—" + cov, n, pct = tot[key] + return f"{pct:.1f}% ({cov}/{n})" + + +def _fmt_delta(b, a, key): + if not b and not a: + return "—" + if not b: + return "**new**" + if not a: + return "**removed**" + d = a[key][2] - b[key][2] + sign = "+" if d >= 0 else "" + return f"**{sign}{d:.1f} pp**" + + +def render( + stats_root: pathlib.Path, + run_url: str, + fuzz_seconds: str, + now_utc: str, + footer: str, +) -> str: + b = _load_variant(stats_root / "baseline") + c = _load_variant(stats_root / "current") + + b_meta = b["meta"] or {} + c_meta = c["meta"] or {} + b_sha_full = b_meta.get("sha") or "" + c_sha_full = c_meta.get("sha") or "" + b_sha = b_sha_full[:7] if b_sha_full else "unknown" + c_sha = c_sha_full[:7] if c_sha_full else "unknown" + project = c_meta.get("project") or b_meta.get("project") or "?" + b_has = bool(b_meta.get("has_project")) + c_has = bool(c_meta.get("has_project")) + + out = [MARKER, "", "## Fuzzing Coverage Report", ""] + + tested = f"**Tested:** project `{project}` · base `{b_sha}`" + if not b_has: + tested += ( + " _(no baseline — project not present at base or baseline build failed)_" + ) + tested += f" → head `{c_sha}`" + if not c_has: + tested += " _(current measurement failed)_" + tested += (f" · {fuzz_seconds}s total fuzz budget" + f" · updated {now_utc}" + f" · [workflow run]({run_url})") + out += [tested, ""] + + bt = _totals(b["project"]) + ct = _totals(c["project"]) + if bt or ct: + out += [ + "| Metric | Before | After | Delta |", + "|---|---|---|---|", + f"| Line coverage | {_fmt_cov(bt, 'lines')} | {_fmt_cov(ct, 'lines')} | {_fmt_delta(bt, ct, 'lines')} |", + f"| Branch coverage | {_fmt_cov(bt, 'branches')} | {_fmt_cov(ct, 'branches')} | {_fmt_delta(bt, ct, 'branches')} |", + f"| Function coverage | {_fmt_cov(bt, 'functions')} | {_fmt_cov(ct, 'functions')} | {_fmt_delta(bt, ct, 'functions')} |", + "", + ] + + all_h = sorted(set(b["harness"].keys()) | set(c["harness"].keys())) + if all_h: + out += [ + "### Per-harness", + "", + "| Harness | Lines before | Lines after | Δ |", + "|---|---|---|---|", + ] + for h in all_h: + bh = _totals(b["harness"].get(h)) + ch = _totals(c["harness"].get(h)) + out.append( + f"| `{h}` | {_fmt_cov(bh, 'lines')} | {_fmt_cov(ch, 'lines')} | " + f"{_fmt_delta(bh, ch, 'lines')} |") + out.append("") + + if not (bt or ct or all_h): + out += [ + "_No coverage data collected. Check the workflow run for build errors._", + "", + ] + + out.append(footer) + return "\n".join(out) + + +def main(): + stats_root = pathlib.Path(os.environ.get("STATS_ROOT", "stats")) + run_url = os.environ["RUN_URL"] + fuzz_seconds = os.environ.get("FUZZ_SECONDS", "300") + footer_kind = os.environ.get("FOOTER", "generic") + now_utc = datetime.datetime.now( + datetime.timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + footer = FOOTER_UPSTREAM if footer_kind == "upstream" else FOOTER_GENERIC + sys.stdout.write(render(stats_root, run_url, fuzz_seconds, now_utc, footer)) + sys.stdout.write("\n") + + +if __name__ == "__main__": + main() diff --git a/.github/actions/regression_tests/action.yml b/.github/actions/regression_tests/action.yml deleted file mode 100644 index f17b04da..00000000 --- a/.github/actions/regression_tests/action.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Regression tests -description: Run regression tests and write summary. - -inputs: - artifact-name: - description: 'Name of the uploaded artifact' - required: false - default: 'regression-test-results' - library-path: - description: 'Path to the InChI library under test' - required: false - default: 'CMake_build/full_build/INCHI-1-SRC/INCHI_API/libinchi/src/lib/libinchi.so' - shell: - description: 'Shell for running the tests' - required: false - default: 'bash' - -runs: - using: "composite" - steps: - - name: Run regression tests - run: python INCHI-1-TEST/tests/test_library/inchi_tests/run_tests.py --test=regression --lib-path=${{ inputs.library-path}} --data-config=INCHI-1-TEST/tests/test_library/config/config_ci.py - shell: ${{ inputs.shell }} - - - name: Write regression summary - if: ${{ !cancelled() }} - run: python INCHI-1-TEST/tests/test_library/inchi_tests/parse_log.py --test=regression --lib-path=${{ inputs.library-path}} --data-config=INCHI-1-TEST/tests/test_library/config/config_ci.py - shell: ${{ inputs.shell }} - - - name: Upload regression test results - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: ${{ inputs.artifact-name }} - path: | - ./INCHI-1-TEST/tests/test_library/data/ci/*_regression_ci.log - ./INCHI-1-TEST/tests/test_library/data/ci/*_regression_ci_*.html diff --git a/.github/fuzz/Dockerfile b/.github/fuzz/Dockerfile new file mode 100644 index 00000000..d1cdfedf --- /dev/null +++ b/.github/fuzz/Dockerfile @@ -0,0 +1,21 @@ +# Copyright 2020 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +FROM gcr.io/oss-fuzz-base/base-builder +RUN apt-get update && apt-get install -y wget make unzip git +RUN git clone --depth=1 https://github.com/IUPAC-InChI/InChI inchi +WORKDIR inchi +COPY run_tests.sh build.sh *_fuzzer.c $SRC/ diff --git a/.github/fuzz/build.sh b/.github/fuzz/build.sh new file mode 100644 index 00000000..825384d2 --- /dev/null +++ b/.github/fuzz/build.sh @@ -0,0 +1,42 @@ +#!/bin/bash -eu +# Copyright 2020 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +pushd INCHI-1-SRC +# Compile library sources (exclude ichimain.c which is the standalone program main) +# Remove -ansi flag since upstream now uses C99 features (loop-scoped variables) +SRC_FILES=$(ls INCHI_BASE/src/*.c INCHI_API/libinchi/src/*.c INCHI_API/libinchi/src/ixa/*.c | grep -v ichimain.c) +$CC $CFLAGS -Wno-everything -DTARGET_API_LIB -c $SRC_FILES + +ar rcs $WORK/libinchi.a *.o + +for fuzzer in $SRC/*_fuzzer.c; do + fuzzer_basename=$(basename -s .c $fuzzer) + + $CC $CFLAGS \ + -I INCHI_BASE/src/ \ + -I INCHI_API/libinchi/src/ \ + -I INCHI_API/libinchi/src/ixa/ \ + $fuzzer -c -o ${fuzzer_basename}.o + + $CXX $CXXFLAGS \ + ${fuzzer_basename}.o -o $OUT/$fuzzer_basename \ + $LIB_FUZZING_ENGINE $WORK/libinchi.a +done +popd + +# Build test +INCHI-1-TEST/build_with_cmake.sh all diff --git a/.github/fuzz/harnesses/inchi_input_fuzzer.c b/.github/fuzz/harnesses/inchi_input_fuzzer.c new file mode 100644 index 00000000..d038da2a --- /dev/null +++ b/.github/fuzz/harnesses/inchi_input_fuzzer.c @@ -0,0 +1,56 @@ +// Copyright 2020 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include "inchi_api.h" + +// Define the maximum value for size_t. We return if the fuzzing input is equal +// to kSizeMax because appending the null-terminator to the InChI buffer would +// cause wraparound, thereby initializing the buffer to size 0. +static const size_t kSizeMax = (size_t)-1; + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + + if (size == kSizeMax) + return 0; + + char *szINCHISource = malloc(sizeof(char) * (size + 1)); + memcpy(szINCHISource, data, size); + szINCHISource[size] = '\0'; // InChI string must be null-terminated + + // Buffer lengths taken from InChI API reference, located at + // https://www.inchi-trust.org/download/104/InChI_API_Reference.pdf, page 24 + char szINCHIKey[28], szXtra1[65], szXtra2[65]; + GetINCHIKeyFromINCHI(szINCHISource, 0, 0, szINCHIKey, szXtra1, szXtra2); + + inchi_InputINCHI inpInChI; + inpInChI.szInChI = szINCHISource; + inpInChI.szOptions = NULL; + + inchi_Output out; + GetINCHIfromINCHI(&inpInChI, &out); + + inchi_OutputStruct outStruct; + GetStructFromINCHI(&inpInChI, &outStruct); + + free(szINCHISource); + FreeINCHI(&out); + FreeStructFromINCHI(&outStruct); + + return 0; +} diff --git a/.github/fuzz/project.yaml b/.github/fuzz/project.yaml new file mode 100644 index 00000000..1a5838b8 --- /dev/null +++ b/.github/fuzz/project.yaml @@ -0,0 +1,12 @@ +homepage: "https://www.inchi-trust.org/" +main_repo: "https://github.com/IUPAC-InChI/InChI" +language: c +primary_contact: "member-info@inchi-trust.org" +sanitizers: + - address + +fuzzing_engines: + - afl + - honggfuzz + - libfuzzer + diff --git a/.github/fuzz/run_tests.sh b/.github/fuzz/run_tests.sh new file mode 100644 index 00000000..e9cfa38c --- /dev/null +++ b/.github/fuzz/run_tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash -eu +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# Excluding unit test case that failed +ctest --test-dir CMake_build/full_build -E "test_inchi_dll_b|test_permutation_util" diff --git a/.github/workflows/fuzz-verify.yml b/.github/workflows/fuzz-verify.yml new file mode 100644 index 00000000..a478d953 --- /dev/null +++ b/.github/workflows/fuzz-verify.yml @@ -0,0 +1,186 @@ +# Template — injected into upstream repo fork branches by pr_server.py at PR submission time. +# Not used directly in this repo. Injected alongside .github/actions/{collect-fuzz-stats,post-fuzz-report}/. +name: Fuzz Verify +on: [push] + +permissions: + # Default to read-only at the workflow level; the report job elevates to pull-requests:write. + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PROJECT: inchi + FUZZ_SECONDS: "300" + +jobs: + measure: + name: Measure (${{ matrix.variant }}) + runs-on: ubuntu-latest + timeout-minutes: 45 + continue-on-error: ${{ matrix.variant == 'baseline' }} + strategy: + fail-fast: false + matrix: + variant: [baseline, current] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + persist-credentials: false + submodules: recursive + + - name: Resolve variant + id: variant + env: + VARIANT: ${{ matrix.variant }} + run: | + if [ "$VARIANT" = "baseline" ]; then + git remote set-head origin -a >/dev/null 2>&1 || true + DEFAULT_BRANCH=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|origin/||') + [ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH=main + if ! git rev-parse --verify "origin/$DEFAULT_BRANCH" >/dev/null 2>&1; then + DEFAULT_BRANCH=master + fi + BASE_SHA=$(git merge-base "origin/$DEFAULT_BRANCH" HEAD) + echo "sha=$BASE_SHA" >> "$GITHUB_OUTPUT" + echo "Baseline: $BASE_SHA (merge-base with origin/$DEFAULT_BRANCH)" + + if [ ! -d .github/fuzz ]; then + echo "::error::.github/fuzz not present on PR — cannot overlay onto baseline" + echo "has_project=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + FUZZ_TMP=$(mktemp -d) + cp -r .github/fuzz "$FUZZ_TMP/" + ACTIONS_TMP=$(mktemp -d) + cp -r .github/actions "$ACTIONS_TMP/" + git reset --hard "$BASE_SHA" + git submodule update --init --recursive + rm -rf .github/fuzz .github/actions + mkdir -p .github + cp -r "$FUZZ_TMP/fuzz" .github/ + cp -r "$ACTIONS_TMP/actions" .github/ + echo "has_project=true" >> "$GITHUB_OUTPUT" + else + echo "sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT" + echo "has_project=true" >> "$GITHUB_OUTPUT" + echo "Current: $GITHUB_SHA" + fi + + - name: Clone oss-fuzz + if: steps.variant.outputs.has_project == 'true' + run: git clone --depth 1 https://github.com/google/oss-fuzz.git /tmp/oss-fuzz + + - name: Install oss-fuzz deps + if: steps.variant.outputs.has_project == 'true' + run: pip install -r /tmp/oss-fuzz/infra/ci/requirements.txt 2>/dev/null || pip install docker + + - name: Set up project + if: steps.variant.outputs.has_project == 'true' + run: | + mkdir -p "/tmp/oss-fuzz/projects/$PROJECT" + cp -r .github/fuzz/* "/tmp/oss-fuzz/projects/$PROJECT/" + + - name: Build image + if: steps.variant.outputs.has_project == 'true' + run: echo n | python3 /tmp/oss-fuzz/infra/helper.py build_image "$PROJECT" + + - name: Build fuzzers + if: steps.variant.outputs.has_project == 'true' + run: echo n | python3 /tmp/oss-fuzz/infra/helper.py build_fuzzers --sanitizer address "$PROJECT" "$GITHUB_WORKSPACE" + + - name: Fuzz + if: steps.variant.outputs.has_project == 'true' + run: | + OUT="/tmp/oss-fuzz/build/out/$PROJECT" + + HARNESSES="" + for f in "$OUT"/*; do + [ -f "$f" ] && [ -x "$f" ] || continue + case "$(basename "$f")" in + *.options|*.dict|*_seed_corpus.zip|llvm-symbolizer) continue ;; + esac + HARNESSES="$HARNESSES $(basename "$f")" + done + HARNESSES=$(echo $HARNESSES | xargs) + + if [ -z "$HARNESSES" ]; then + echo "::error::No harness binaries found in $OUT" + exit 1 + fi + + N=$(echo $HARNESSES | wc -w) + TIME_EACH=$(( FUZZ_SECONDS / N )) + [ "$TIME_EACH" -lt 30 ] && TIME_EACH=30 + echo "Harnesses ($N): $HARNESSES — ${TIME_EACH}s each" + + for h in $HARNESSES; do + echo "::group::$h" + mkdir -p "/tmp/oss-fuzz/corpus/$PROJECT/$h" + timeout $((TIME_EACH + 60)) python3 /tmp/oss-fuzz/infra/helper.py run_fuzzer \ + --corpus-dir "/tmp/oss-fuzz/corpus/$PROJECT/$h" \ + "$PROJECT" "$h" -- \ + "-max_total_time=$TIME_EACH" 2>&1 || true + echo "Corpus: $(find "/tmp/oss-fuzz/corpus/$PROJECT/$h" -type f | wc -l) files" + echo "::endgroup::" + done + + - name: Coverage + if: steps.variant.outputs.has_project == 'true' + run: | + echo n | python3 /tmp/oss-fuzz/infra/helper.py build_fuzzers --sanitizer coverage "$PROJECT" "$GITHUB_WORKSPACE" + + mkdir -p "/tmp/oss-fuzz/build/corpus" + ln -sfn "/tmp/oss-fuzz/corpus/$PROJECT" "/tmp/oss-fuzz/build/corpus/$PROJECT" + + python3 /tmp/oss-fuzz/infra/helper.py coverage \ + --no-corpus-download --no-serve \ + "$PROJECT" 2>&1 || true + + - name: Collect stats + if: always() + uses: ./.github/actions/collect-fuzz-stats + with: + project: ${{ env.PROJECT }} + variant: ${{ matrix.variant }} + sha: ${{ steps.variant.outputs.sha }} + has_project: ${{ steps.variant.outputs.has_project }} + out_base: /tmp/oss-fuzz/build/out + + - name: Upload stats + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: stats-${{ matrix.variant }} + path: stats-${{ matrix.variant }}/ + if-no-files-found: error + + - name: Upload coverage report + if: always() && steps.variant.outputs.has_project == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-report-${{ matrix.variant }} + path: /tmp/oss-fuzz/build/out/*coverage*/report/ + if-no-files-found: ignore + + report: + name: Report + needs: [measure] + if: always() + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # post sticky fuzz-verify comment + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + sparse-checkout: .github/actions/post-fuzz-report + - uses: ./.github/actions/post-fuzz-report + with: + footer: upstream + fuzz_seconds: ${{ env.FUZZ_SECONDS }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/INCHI-1-SRC/INCHI_BASE/src/ichi_bns.c b/INCHI-1-SRC/INCHI_BASE/src/ichi_bns.c index 04af6d7a..bb3c4339 100644 --- a/INCHI-1-SRC/INCHI_BASE/src/ichi_bns.c +++ b/INCHI-1-SRC/INCHI_BASE/src/ichi_bns.c @@ -5970,7 +5970,7 @@ int mark_alt_bonds_and_taut_groups( struct tagINCHI_CLOCK *ic, if (at[0].iso_atw_diff) { #if ( FIX_CURE53_ISSUE_NULL_DEREFERENCE_MAKE_A_COPY_OF_T_GROUP_INFO==1 || defined(FIX_IMPOSSIBLE_H_ISOTOPE_BUG) ) - if (at[0].iso_atw_diff <= NUM_H_ISOTOPES) + if (at[0].iso_atw_diff > 0 && at[0].iso_atw_diff <= NUM_H_ISOTOPES) { /* djb-rwth: possible false positive oss-fuzz issue #39064660 */ t_group_info->tni.nNumRemovedProtonsIsotopic[at[0].iso_atw_diff - 1] ++;