From dc40c2f86c3f201e3203d2ec0b092a18cbe25f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 7 Oct 2026 13:35:45 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(fuzz):=20stop=20XPath=20and=20?= =?UTF-8?q?XSLT=20at=20an=20op=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A super-linear XPath expression or XSLT stylesheet runs until the fuzz harness times out, and the timeout hides every other finding in that run. libxslt bounds its own fuzzers the same way: it charges each parsed and each applied instruction (xslt.c, transform.c) and libxml2 charges each XPath operation and yielded node (xpath.c xmlXPathCheckOpLimit). The fuzz environments now compile with -DTH_OPERATION_LIMIT=100000, the XPath limit libxslt's fuzzer sets (tests/fuzz/fuzz.c). XPath evaluation, stylesheet compilation and stylesheet application each count steps and raise ValueError past the limit. Release builds leave the macro undefined; their preprocessed sources differ from upstream only by trailing commas in struct initializers, so the CodSpeed gate measures the same code. fuzz-smoke runs tests/fuzz_build against the limited build; the coverage matrix skips that directory because its build compiles no counter. Refs #1016 --- docs/development/index.rst | 5 + pyproject.toml | 1 + src/turbohtml/_c/query/xpath/eval.c | 114 +++++++++++++++-------- src/turbohtml/_c/query/xpath/internal.h | 17 ++++ src/turbohtml/_c/query/xslt.c | 34 +++++++ tests/fuzz_build/test_operation_limit.py | 84 +++++++++++++++++ tools/fuzz/fuzz.py | 33 +++++-- tox.toml | 15 ++- 8 files changed, 250 insertions(+), 53 deletions(-) create mode 100644 tests/fuzz_build/test_operation_limit.py diff --git a/docs/development/index.rst b/docs/development/index.rst index 229bc1baf..2294d9162 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -147,6 +147,11 @@ ship this build. The ``preprocess-identity`` environment checks that its branche byte-identical, and the ``scalar`` environment runs the suite with ``-Dforce_scalar=true``, which swaps the SSE2 and NEON scan loops for the portable SWAR ones. +The fuzz environments compile the extension with ``-DTH_OPERATION_LIMIT=100000``. XPath evaluation, XSLT stylesheet +compilation and XSLT application then count their steps and raise ``ValueError`` past the limit, so a known super-linear +input fails fast instead of timing out and hiding the next find. Release builds leave the macro undefined and compile no +counter. ``fuzz-smoke`` runs ``tests/fuzz_build/`` to check that the limit stops those inputs. + The in-process driver runs each input under pymalloc and again under ``PYTHONMALLOC=malloc``, because AddressSanitizer cannot see an over-read that stays inside a pymalloc pool. The deep run splits ``--minutes`` between the two passes. Both environments pin ``PYTHONHASHSEED=0``. ``--rng-seed`` (default ``$FUZZ_RNG_SEED``, else 0) fixes the mutation diff --git a/pyproject.toml b/pyproject.toml index f2cfdf84b..25dcd5b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -403,6 +403,7 @@ run.omit = [ "tests/conformance/test_cssom_jsdom_conformance.py", # differential oracle: skips when the node/jsdom toolchain is absent "tests/conformance/test_dom_jsdom_differential.py", "tests/conformance/test_xml_conformance.py", + "tests/fuzz_build/*", # needs the operation limit only the fuzz-smoke build compiles in ] run.parallel = true run.plugins = [ diff --git a/src/turbohtml/_c/query/xpath/eval.c b/src/turbohtml/_c/query/xpath/eval.c index bffb17658..e0fb10a51 100644 --- a/src/turbohtml/_c/query/xpath/eval.c +++ b/src/turbohtml/_c/query/xpath/eval.c @@ -1044,23 +1044,28 @@ static int apply_predicates(const xp_program *prog, int32_t pred_head, xp_ctx *c Py_ssize_t size = set->len; Py_ssize_t write_pos = 0; for (Py_ssize_t index = 0; index < set->len; index++) { - xp_ctx pctx = {ctx->tree, - set->items[index].node, - set->items[index].attr, - index + 1, - size, - ctx->feature, - ctx->vars, - ctx->namespaces, - ctx->extension, - ctx->extension_ctx, - ctx->depth, - ctx->regex_cache, - ctx->live, - ctx->before_python, - ctx->name_test, - ctx->name_test_ctx, - ctx->strict_no_ns}; + xp_ctx pctx = { + ctx->tree, + set->items[index].node, + set->items[index].attr, + index + 1, + size, + ctx->feature, + ctx->vars, + ctx->namespaces, + ctx->extension, + ctx->extension_ctx, + ctx->depth, + ctx->regex_cache, + ctx->live, + ctx->before_python, + ctx->name_test, + ctx->name_test_ctx, + ctx->strict_no_ns, +#ifdef TH_OPERATION_LIMIT + ctx->operations, +#endif + }; xp_result value; int rc = eval_expr(prog, expr, &pctx, &value); if (rc < 0) { @@ -1286,6 +1291,11 @@ static int eval_path_step_inner(const xp_program *prog, const xn *step, const st if (stepped < 0) { /* GCOVR_EXCL_BR_LINE: alloc */ return -1; /* GCOVR_EXCL_LINE */ } +#ifdef TH_OPERATION_LIMIT + if (xp_charge(ctx, 1 + (size_t)(following->len - before)) < 0) { + return -3; + } +#endif if (step->first >= 0) { xp_nodeset slice = {following->items + before, following->len - before, 0, following->snapshots}; int rc; @@ -1810,6 +1820,11 @@ int eval_expr(const xp_program *prog, int32_t idx, xp_ctx *ctx, xp_result *out) *ctx->feature = "an expression nested too deeply"; return -3; } +#ifdef TH_OPERATION_LIMIT + if (xp_charge(ctx, 1) < 0) { + return -3; + } +#endif ctx->depth++; int rc; if (ctx->live == NULL) { @@ -1828,8 +1843,16 @@ int xp_eval_at(const xp_program *prog, struct th_tree *tree, struct th_node *con const xp_bindings *vars, const xp_namespaces *namespaces, xp_extension_fn extension, void *extension_ctx, xp_result *out, const char **feature) { xr_cache *regex_cache = NULL; - xp_ctx ctx = {tree, context, -1, pos, size, feature, vars, namespaces, extension, - extension_ctx, 0, ®ex_cache, NULL, NULL, NULL, NULL, 0}; +#ifdef TH_OPERATION_LIMIT + size_t operations = 0; +#endif + xp_ctx ctx = { + tree, context, -1, pos, size, feature, vars, namespaces, extension, + extension_ctx, 0, ®ex_cache, NULL, NULL, NULL, NULL, 0, +#ifdef TH_OPERATION_LIMIT + &operations, +#endif + }; int rc = eval_expr(prog, prog->root, &ctx, out); if (regex_cache != NULL) { xr_cache_free(regex_cache); @@ -1841,23 +1864,31 @@ int xp_eval_pattern_at(const xp_program *prog, struct th_tree *tree, struct th_n void *extension_ctx, xp_name_test_fn name_test, void *name_test_ctx, xp_result *out, const char **feature) { xr_cache *regex_cache = NULL; - xp_ctx ctx = {tree, - context, - -1, - 1, - 1, - feature, - NULL, - NULL, - extension, - extension_ctx, - 0, - ®ex_cache, - NULL, - NULL, - name_test, - name_test_ctx, - name_test == NULL && th_tree_is_xml(tree)}; /* GCOVR_EXCL_BR_LINE: XML only */ +#ifdef TH_OPERATION_LIMIT + size_t operations = 0; +#endif + xp_ctx ctx = { + tree, + context, + -1, + 1, + 1, + feature, + NULL, + NULL, + extension, + extension_ctx, + 0, + ®ex_cache, + NULL, + NULL, + name_test, + name_test_ctx, + name_test == NULL && th_tree_is_xml(tree), /* GCOVR_EXCL_BR_LINE: XML only */ +#ifdef TH_OPERATION_LIMIT + &operations, +#endif + }; int rc = eval_expr(prog, prog->root, &ctx, out); if (regex_cache != NULL) { xr_cache_free(regex_cache); @@ -1876,9 +1907,16 @@ int xp_eval_snapshot(const xp_program *prog, struct th_tree *tree, struct th_nod xp_before_python_fn before_python, xp_result *out, const char **feature) { xr_cache *regex_cache = NULL; xp_live_registry live = {0}; +#ifdef TH_OPERATION_LIMIT + size_t operations = 0; +#endif xp_ctx ctx = { - tree, context, -1, 1, 1, feature, vars, namespaces, extension, extension_ctx, 0, ®ex_cache, - &live, before_python, NULL, NULL, 0}; + tree, context, -1, 1, 1, feature, vars, namespaces, extension, extension_ctx, 0, ®ex_cache, + &live, before_python, NULL, NULL, 0, +#ifdef TH_OPERATION_LIMIT + &operations, +#endif + }; xp_live_frame frame = {.node = context, .vars = vars}; xp_live_enter(&ctx, &frame); int rc = eval_expr(prog, prog->root, &ctx, out); diff --git a/src/turbohtml/_c/query/xpath/internal.h b/src/turbohtml/_c/query/xpath/internal.h index ee175e670..aa38e1791 100644 --- a/src/turbohtml/_c/query/xpath/internal.h +++ b/src/turbohtml/_c/query/xpath/internal.h @@ -189,6 +189,9 @@ typedef struct { xp_name_test_fn name_test; void *name_test_ctx; int strict_no_ns; +#ifdef TH_OPERATION_LIMIT + size_t *operations; /* shared by the predicate contexts of one top-level evaluation */ +#endif } xp_ctx; struct xp_live_frame { @@ -227,6 +230,20 @@ static inline int xp_before_python(xp_ctx *ctx) { return ctx->before_python == NULL ? 0 : ctx->before_python(ctx->extension_ctx, ctx->live->current); } +#ifdef TH_OPERATION_LIMIT +/* Fuzz builds pass -DTH_OPERATION_LIMIT so a super-linear expression stops with a ValueError instead of a timeout that + hides other findings. libxml2 charges each evaluated operation and each node a step yields the same way + (xpath.c xmlXPathCheckOpLimit); release builds compile none of this. */ +static inline int xp_charge(xp_ctx *ctx, size_t count) { + *ctx->operations += count; + if (*ctx->operations <= TH_OPERATION_LIMIT) { + return 0; + } + *ctx->feature = "evaluation exceeded the operation limit"; + return -3; +} +#endif + /* Pre-order successor, shared by the evaluator and id(). ns_push is declared in the public xpath.h because the marshaling boundary also builds node-sets through it. */ struct th_node *document_next(struct th_node *node); diff --git a/src/turbohtml/_c/query/xslt.c b/src/turbohtml/_c/query/xslt.c index d937b3e7b..4deb0b7e8 100644 --- a/src/turbohtml/_c/query/xslt.c +++ b/src/turbohtml/_c/query/xslt.c @@ -758,6 +758,9 @@ typedef struct engine { const char *error; int py_error; int owns_model; +#ifdef TH_OPERATION_LIMIT + size_t operations; /* stylesheet elements compiled, then instructions applied in one run */ +#endif } engine; typedef struct { @@ -929,6 +932,19 @@ static int fail_py(engine *eng) { return -1; } +#ifdef TH_OPERATION_LIMIT +/* Fuzz builds pass -DTH_OPERATION_LIMIT so a super-linear stylesheet stops with a ValueError instead of a timeout that + hides other findings. libxslt charges each parsed instruction (xslt.c xsltParseSequenceConstructor) and each + instantiated one (transform.c xsltApplySequenceConstructor) the same way; release builds compile none of this. */ +static int charge_operation(engine *eng, const char *phase) { + if (++eng->operations <= TH_OPERATION_LIMIT) { + return 0; + } + PyErr_Format(PyExc_ValueError, "xslt: %s exceeded the operation limit of %d", phase, TH_OPERATION_LIMIT); + return fail_py(eng); +} +#endif + /* ---- compile a match pattern to an equivalent absolute expression --------- */ /* Split a pattern on top-level '|' (outside brackets, parentheses and string @@ -4833,6 +4849,11 @@ static int instantiate_body(engine *eng, th_node *body, th_node *out_parent) { if (is_xsl_dynamic(eng, child, "param") || is_xsl_dynamic(eng, child, "sort")) { continue; } +#ifdef TH_OPERATION_LIMIT + if ((rc = charge_operation(eng, "applying the stylesheet")) < 0) { + break; + } +#endif rc = instantiate_one_dynamic(eng, child, out_parent); } } else { @@ -4842,6 +4863,11 @@ static int instantiate_body(engine *eng, th_node *body, th_node *out_parent) { if (is_xsl_fast(eng, child, "param") || is_xsl_fast(eng, child, "sort")) { continue; } +#ifdef TH_OPERATION_LIMIT + if ((rc = charge_operation(eng, "applying the stylesheet")) < 0) { + break; + } +#endif rc = instantiate_one_fast(eng, child, out_parent); } } @@ -5625,6 +5651,9 @@ static int engine_start_run(engine *eng, const engine *model, th_tree *src_tree, eng->ns_counter = 0; eng->gen_counter = 0; eng->depth = 0; +#ifdef TH_OPERATION_LIMIT + eng->operations = 0; +#endif eng->number_count_match = (xslt_number_match){0}; eng->number_from_match = (xslt_number_match){0}; eng->explicit_any = (xslt_number_prefix){0}; @@ -6099,6 +6128,11 @@ static int precompile_stylesheet(engine *eng, th_node *root) { if (node->type != TH_NODE_ELEMENT) { continue; } +#ifdef TH_OPERATION_LIMIT + if (charge_operation(eng, "compiling the stylesheet") < 0) { + return -1; + } +#endif if (node_is_xsl(eng, node)) { if (precompile_instruction(eng, node) < 0) { return -1; diff --git a/tests/fuzz_build/test_operation_limit.py b/tests/fuzz_build/test_operation_limit.py new file mode 100644 index 000000000..caa27b947 --- /dev/null +++ b/tests/fuzz_build/test_operation_limit.py @@ -0,0 +1,84 @@ +"""Fuzz builds stop super-linear XPath and XSLT work at the native operation limit instead of timing out.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +import pytest + +from turbohtml import parse_xml +from turbohtml.transform import Transform + +if TYPE_CHECKING: + from collections.abc import Callable + + from turbohtml import Document + +_SIBLINGS: Final = parse_xml("" + "" * 100 + "") +_NESTED: Final = "count(//a[count(//a[count(//a) > 0]) > 0])" +_DOUBLING: Final = parse_xml( + '' + '' + '' + '' + '' + '' + '' + '' + 'x' +) + + +@pytest.mark.parametrize( + ("run", "expected"), + [ + pytest.param(lambda: str(_SIBLINGS.xpath("count(//a[count(//a) > 0])")), "100.0", id="xpath"), + pytest.param(lambda: Transform(_DOUBLING)(_SIBLINGS, depth="10"), "x" * 1024, id="xslt-apply"), + pytest.param(lambda: Transform(_literal_sheet(100))(_SIBLINGS), "x" * 100, id="xslt-compile"), + ], +) +def test_operation_limit_within_budget(run: Callable[[], str], expected: str) -> None: + assert run() == expected + + +@pytest.mark.parametrize( + ("run", "message"), + [ + pytest.param( + lambda: str(_SIBLINGS.xpath(_NESTED)), r"^xpath: evaluation exceeded the operation limit$", id="xpath" + ), + pytest.param( + lambda: Transform(_DOUBLING)(_SIBLINGS, depth="17"), + r"^xslt: applying the stylesheet exceeded the operation limit of \d+$", + id="xslt-apply", + ), + pytest.param( + lambda: Transform(_value_of_sheet(_NESTED))(_SIBLINGS), + r"^xslt: expression error \(evaluation exceeded the operation limit\)$", + id="xslt-select", + ), + pytest.param( + lambda: Transform(_literal_sheet(200_000))(_SIBLINGS), + r"^xslt: compiling the stylesheet exceeded the operation limit of \d+$", + id="xslt-compile", + ), + ], +) +def test_operation_limit_stops(run: Callable[[], str], message: str) -> None: + with pytest.raises(ValueError, match=message): + run() + + +def _literal_sheet(elements: int) -> Document: + return parse_xml( + '' + f'{"x" * elements}' + ) + + +def _value_of_sheet(expression: str) -> Document: + return parse_xml( + '' + f'", ">")}"/>' + "" + ) diff --git a/tools/fuzz/fuzz.py b/tools/fuzz/fuzz.py index f598724a0..a1a452f4a 100644 --- a/tools/fuzz/fuzz.py +++ b/tools/fuzz/fuzz.py @@ -12,13 +12,14 @@ fault aborts the interpreter with a stack trace. It calls the public API, so it survives the in-flight C refactors. ``smoke`` replays the past finds under ``tests/fuzz_regressions`` and a benign seed corpus once (fast, deterministic, -gates every PR). ``deep`` adds a mutation loop and structural probes for a per-target budget (the scheduled/manual run -that hunts for crashes). ``oracle`` runs the sanitizer wrong-output oracles (``sanitize_oracles.py``) instead, -because a sanitizer bug usually returns unsafe markup without crashing. ``round-trip`` runs the printer, minifier, -entry-point and source-span oracles (``round_trip_oracles.py``) and ``release-diff`` compares HEAD with the latest PyPI -release (``release_diff.py``), for the bugs that return wrong text. A crashing input lands in ``--crash-dir`` as -``crash-``, and the log names it only by hash, length and harness, because CI logs on a public repository are -public. The in-process extension is expected to be pre-built by the tox env; ``--build`` builds it here for a local run. +gates every PR), then runs ``tests/fuzz_build`` against the operation limit the fuzz build compiles in. ``deep`` adds a +mutation loop and structural probes for a per-target budget (the scheduled/manual run that hunts for crashes). +``oracle`` runs the sanitizer wrong-output oracles (``sanitize_oracles.py``) instead, because a sanitizer bug usually +returns unsafe markup without crashing. ``round-trip`` runs the printer, minifier, entry-point and source-span oracles +(``round_trip_oracles.py``) and ``release-diff`` compares HEAD with the latest PyPI release (``release_diff.py``), for +the bugs that return wrong text. A crashing input lands in ``--crash-dir`` as ``crash-``, and the log names it +only by hash, length and harness, because CI logs on a public repository are public. The in-process extension is +expected to be pre-built by the tox env; ``--build`` builds it here for a local run. ``smoke`` and ``deep`` build everything in the fuzz-only mode (meson ``-Dfuzzing=true``) and start with a self-test: each ``_fuzz_crash`` kind must draw its AddressSanitizer report, and an injected allocation failure must raise @@ -101,9 +102,11 @@ def main(argv: Sequence[str] | None = None) -> int: return _run_round_trip(args.mode, args.minutes, args.rng_seed, args.crash_dir, passthrough) if not args.skip_inprocess and (code := _self_test()) != 0: return code - if (code := _run_standalone(args.mode, args.extra_corpus, args.crash_dir)) != 0: + if (code := _run_standalone(args.mode, args.extra_corpus, args.crash_dir)) != 0 or args.skip_inprocess: return code - return 0 if args.skip_inprocess else _run_inprocess(args.mode, args.minutes, args.rng_seed, args.crash_dir) + return _run_inprocess(args.mode, args.minutes, args.rng_seed, args.crash_dir) or ( + _run_operation_limit() if args.mode == "smoke" else 0 + ) def _build_extension(build_dir: Path) -> None: @@ -117,7 +120,7 @@ def _build_extension(build_dir: Path) -> None: "--editable", str(_ROOT), f"--config-settings=build-dir={build_dir}", - "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined", + "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined -DTH_OPERATION_LIMIT=100000", "--config-settings=setup-args=-Dc_link_args=-fsanitize=address,undefined", "--config-settings=setup-args=-Dbuildtype=debugoptimized", "--config-settings=setup-args=-Dfuzzing=true", @@ -380,6 +383,16 @@ def _run_inprocess(mode: str, minutes: float, rng_seed: int, crash_dir: Path) -> return status +def _run_operation_limit() -> int: + # the release build compiles no counters, so these checks run only here, against the fuzz build + return subprocess.run( + [sys.executable, "-m", "pytest", str(_ROOT / "tests" / "fuzz_build"), "--no-cov", "-p", "no:cacheprovider"], + cwd=_ROOT, + env={"PYTHONHASHSEED": "0", **_asan_preload()}, + check=False, + ).returncode + + def _asan_preload() -> dict[str, str]: """Build the env preloading the ASan runtime ahead of the interpreter so the instrumented .so's interceptors arm.""" on_linux = platform.system() == "Linux" diff --git a/tox.toml b/tox.toml index 4b2fd458b..194c08a57 100644 --- a/tox.toml +++ b/tox.toml @@ -86,6 +86,8 @@ commands = [ # env (the "🔬 conformance" CI job). Deselect them here so the normal matrix never runs an oracle suite # without its oracle -- a missing oracle must error there, not skip silently in the matrix. "--ignore={tox_root}{/}tests{/}conformance", + # tests/fuzz_build checks the operation limit only the sanitizer fuzz build compiles in; fuzz-smoke runs it + "--ignore={tox_root}{/}tests{/}fuzz_build", { replace = "posargs", default = [ "tests" ], extend = true }, ], # enforce 100% C LINE coverage everywhere gcov runs, and 100% BRANCH coverage on @@ -281,6 +283,7 @@ commands = [ "10", "--no-cov", "--ignore={tox_root}{/}tests{/}conformance", + "--ignore={tox_root}{/}tests{/}fuzz_build", { replace = "posargs", default = [ "tests" ], extend = true }, ], ] @@ -467,7 +470,7 @@ commands_pre = [ "--editable", "{tox_root}", "--config-settings=build-dir={env_dir}{/}cbuild", - "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined", + "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined -DTH_OPERATION_LIMIT=100000", "--config-settings=setup-args=-Dc_link_args=-fsanitize=address,undefined", "--config-settings=setup-args=-Dbuildtype=debugoptimized", "--config-settings=setup-args=-Dfuzzing=true", @@ -549,7 +552,7 @@ commands_pre = [ "--editable", "{tox_root}", "--config-settings=build-dir={env_dir}{/}cbuild", - "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined", + "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined -DTH_OPERATION_LIMIT=100000", "--config-settings=setup-args=-Dc_link_args=-fsanitize=address,undefined", "--config-settings=setup-args=-Dbuildtype=debugoptimized", ], @@ -617,7 +620,7 @@ commands_pre = [ "--editable", "{tox_root}", "--config-settings=build-dir={env_dir}{/}cbuild", - "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined", + "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined -DTH_OPERATION_LIMIT=100000", "--config-settings=setup-args=-Dc_link_args=-fsanitize=address,undefined", "--config-settings=setup-args=-Dbuildtype=debugoptimized", ], @@ -641,7 +644,8 @@ set_env = { CC = "clang", PYTHONHASHSEED = "0" } commands_pre = [ # build the extension with the sanitizers so the in-process harness faults on a C memory error. b_sanitize trips # meson's linker capability check for an extension module on macOS, so pass the flag through c_args/c_link_args, which - # the driver's ASan-runtime preload then arms; buildtype matches the tsan env's instrumented build. + # the driver's ASan-runtime preload then arms; buildtype matches the tsan env's instrumented build. The operation + # limit is the XPath opLimit libxslt's own fuzzer sets (libxslt tests/fuzz/fuzz.c), applied to each counter. [ "uv", "pip", @@ -652,7 +656,7 @@ commands_pre = [ "--editable", "{tox_root}", "--config-settings=build-dir={env_dir}{/}cbuild", - "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined", + "--config-settings=setup-args=-Dc_args=-fsanitize=address,undefined -DTH_OPERATION_LIMIT=100000", "--config-settings=setup-args=-Dc_link_args=-fsanitize=address,undefined", "--config-settings=setup-args=-Dbuildtype=debugoptimized", "--config-settings=setup-args=-Dfuzzing=true", @@ -737,6 +741,7 @@ commands = [ "pytest", "--no-cov", "--ignore={tox_root}{/}tests{/}conformance", + "--ignore={tox_root}{/}tests{/}fuzz_build", { replace = "posargs", default = [ "tests" ], extend = true }, ], ]