Seed Thompson-sampling bandit with informative priors from static target profiling - #1
Conversation
Reviewer's GuideIntroduces informative Beta priors for the Thompson-sampling MonteCarloScheduler using static target profiling, wires those priors through arm registration for schedulers that support them, and documents and tests the new behavior. Sequence diagram for registering arms with informative priorssequenceDiagram
participant Fuzzer
participant TargetProfiler
participant MonteCarloScheduler
participant MOptScheduler
Fuzzer->>TargetProfiler: format_operator_priors(profile)
TargetProfiler-->>Fuzzer: priors
Fuzzer->>MonteCarloScheduler: _register_arms(scheduler=mc, priors=priors)
activate MonteCarloScheduler
MonteCarloScheduler-->>MonteCarloScheduler: supports_priors = True
MonteCarloScheduler-->>MonteCarloScheduler: init_arm(op, prior_alpha, prior_beta)
deactivate MonteCarloScheduler
Fuzzer->>MOptScheduler: _register_arms(scheduler=mopt, priors=priors)
activate MOptScheduler
MOptScheduler-->>MOptScheduler: supports_priors (missing or False)
MOptScheduler-->>MOptScheduler: init_arm(op)
deactivate MOptScheduler
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
_register_arms, invalidpriorsentries (e.g. wrong-length tuples or non-numeric values) are silently ignored and fall back to default behavior; consider adding explicit validation/logging so configuration mistakes are surfaced rather than quietly degraded. - When clamping
prior_alpha/prior_betainMonteCarloScheduler.init_arm, you currently modify bad priors silently; adding a debug log (or raising in strict modes) when values are clamped would make it easier to detect misconfigured priors without affecting the default behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `_register_arms`, invalid `priors` entries (e.g. wrong-length tuples or non-numeric values) are silently ignored and fall back to default behavior; consider adding explicit validation/logging so configuration mistakes are surfaced rather than quietly degraded.
- When clamping `prior_alpha`/`prior_beta` in `MonteCarloScheduler.init_arm`, you currently modify bad priors silently; adding a debug log (or raising in strict modes) when values are clamped would make it easier to detect misconfigured priors without affecting the default behavior.
## Individual Comments
### Comment 1
<location path="src/fuzzer_tool/core/target_profiler.py" line_range="619-626" />
<code_context>
+
+# Format signature -> structure-aware mutation operators that are almost
+# certainly useful for that format (see core.mutations.FORMAT_MUTATIONS).
+_FORMAT_OPERATOR_HINTS: dict[str, tuple[str, ...]] = {
+ "png": ("png_chunk_mutate", "png_crc_fix"),
+ "jpeg": ("jpeg_chunk_mutate", "jpeg_crc_fix"),
+ "gzip": ("gzip_chunk_mutate",),
+ "bz2": ("gzip_chunk_mutate",),
+ "xz": ("gzip_chunk_mutate",),
+ "zlib": ("zlib_chunk_mutate",),
+ "riff": ("bmp_chunk_mutate",),
+}
+
</code_context>
<issue_to_address>
**suggestion:** Consider normalizing/relaxing format_signature matching to reduce brittleness.
The lookup in `_FORMAT_OPERATOR_HINTS` uses `profile.format_signature` verbatim, so case or representation differences (e.g. `PNG`, `image/png`, `riff/wav`) will prevent matches and skip the boosted priors. If the canonical form isn’t already enforced upstream, consider normalizing `format_signature` before lookup (e.g. lowercasing, stripping MIME prefixes, alias mapping) or clearly documenting the expected canonical format here.
Suggested implementation:
```python
# Format signature -> structure-aware mutation operators that are almost
# certainly useful for that format (see core.mutations.FORMAT_MUTATIONS).
#
# The lookup below expects a *normalized* format signature. Call
# `_normalize_format_signature(profile.format_signature)` before using
# `_FORMAT_OPERATOR_HINTS` to avoid brittleness due to case, MIME-style
# prefixes (e.g. `image/png`), or common aliases (`jpg` vs `jpeg`,
# `riff/wav` vs `riff`, etc.).
def _normalize_format_signature(signature: str) -> str:
"""Normalize a format signature into the canonical key used in
`_FORMAT_OPERATOR_HINTS`.
This is intentionally conservative; it lowercases, strips common
MIME-style prefixes, and applies a small alias map for widely-used
variants. If a format is not recognized, the original (lowercased)
signature is returned, so adding new formats remains straightforward.
"""
sig = signature.strip().lower()
if not sig:
return sig
# Common alias mappings for well-known formats.
alias_map = {
# PNG
"png": "png",
"image/png": "png",
# JPEG
"jpeg": "jpeg",
"jpg": "jpeg",
"image/jpeg": "jpeg",
# RIFF container (e.g. WAV)
"riff": "riff",
"riff/wav": "riff",
"wav": "riff",
# Compression formats
"gzip": "gzip",
"application/gzip": "gzip",
"bz2": "bz2",
"bzip2": "bz2",
"xz": "xz",
"application/x-xz": "xz",
"zlib": "zlib",
}
# Direct alias match first.
if sig in alias_map:
return alias_map[sig]
# For MIME-like strings, prefer the subtype as the canonical key.
if "/" in sig:
main, subtype = sig.split("/", 1)
subtype = subtype.strip().lower()
if subtype in alias_map:
return alias_map[subtype]
# Fall back to subtype if we don't know it explicitly.
return subtype
return sig
_FORMAT_OPERATOR_HINTS: dict[str, tuple[str, ...]] = {
"png": ("png_chunk_mutate", "png_crc_fix"),
"jpeg": ("jpeg_chunk_mutate", "jpeg_crc_fix"),
"gzip": ("gzip_chunk_mutate",),
"bz2": ("gzip_chunk_mutate",),
"xz": ("gzip_chunk_mutate",),
"zlib": ("zlib_chunk_mutate",),
}
```
1. Wherever `_FORMAT_OPERATOR_HINTS` is consulted (likely something like `_FORMAT_OPERATOR_HINTS.get(profile.format_signature, ...)`), update the lookup to normalize first, e.g.:
- Replace `profile.format_signature` with `_normalize_format_signature(profile.format_signature)` in the key used for `_FORMAT_OPERATOR_HINTS`.
2. If the profiler or `Profile` type has documentation/comments about `format_signature`, update them to mention that signatures are normalized via `_normalize_format_signature` and give examples of accepted forms (`PNG`, `image/png`, `riff/wav`, `jpg`).
</issue_to_address>
### Comment 2
<location path="src/fuzzer_tool/core/montecarlo.py" line_range="93-102" />
<code_context>
+ def init_arm(self, name: str, prior_alpha: float = 1.0, prior_beta: float = 1.0) -> None:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clarify the documented contract for prior parameters vs. the clamping behavior.
The docstring states prior_alpha/prior_beta "must be > 0", but the implementation clamps them with max(prior_*, MIN_BETA_PARAM). This lets callers pass 0 or negative values that are silently coerced to MIN_BETA_PARAM. Please either document this clamping behavior explicitly or consider raising/logging on non-positive inputs to surface configuration errors earlier.
Suggested implementation:
```python
def init_arm(self, name: str, prior_alpha: float = 1.0, prior_beta: float = 1.0) -> None:
"""Register a mutation operator arm with a Beta prior.
Defaults to the uninformative Beta(1, 1) prior. Callers with prior
knowledge about an operator's likely usefulness (e.g. static target
profiling indicating a specific file format) can pass a stronger
prior to bias early Thompson sampling before any evidence has been
observed. A no-op if the arm is already registered — the prior only
applies at first registration and is never overwritten by later
calls, matching the existing idempotent behavior of this method.
The `prior_alpha` and `prior_beta` parameters are expected to be
strictly positive. For robustness, the implementation clamps
non‑positive values up to the internal minimum Beta parameter
`MIN_BETA_PARAM` via `max(prior_*, MIN_BETA_PARAM)`. This means that
passing 0 or negative values will not raise, but will instead behave
as if `prior_* == MIN_BETA_PARAM`.
```
If elsewhere in this docstring (or in parameter docs not shown in the snippet) you explicitly state that `prior_alpha` and `prior_beta` "must be > 0" without mentioning clamping, update that text to reference the clamping behavior described here to keep the contract consistent.
</issue_to_address>
### Comment 3
<location path="tests/test_target_profiler.py" line_range="194-203" />
<code_context>
+class TestFormatOperatorPriors:
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test that `interesting_strings` alone also boost dictionary-aware operators
You currently test `magic_bytes` and `boundary_markers` boosting `_DICT_OPERATORS`, but not `interesting_strings`, which is also part of the condition. Please add a test like:
```python
def test_interesting_strings_boost_dict_operators(self):
p = TargetProfile()
p.interesting_strings = [b"token"]
priors = format_operator_priors(p)
assert "dict_insert" in priors
assert "dict_append" in priors
```
to exercise the `interesting_strings` branch of `if profile.magic_bytes or profile.boundary_markers or profile.interesting_strings:`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| _FORMAT_OPERATOR_HINTS: dict[str, tuple[str, ...]] = { | ||
| "png": ("png_chunk_mutate", "png_crc_fix"), | ||
| "jpeg": ("jpeg_chunk_mutate", "jpeg_crc_fix"), | ||
| "gzip": ("gzip_chunk_mutate",), | ||
| "bz2": ("gzip_chunk_mutate",), | ||
| "xz": ("gzip_chunk_mutate",), | ||
| "zlib": ("zlib_chunk_mutate",), | ||
| "riff": ("bmp_chunk_mutate",), |
There was a problem hiding this comment.
suggestion: Consider normalizing/relaxing format_signature matching to reduce brittleness.
The lookup in _FORMAT_OPERATOR_HINTS uses profile.format_signature verbatim, so case or representation differences (e.g. PNG, image/png, riff/wav) will prevent matches and skip the boosted priors. If the canonical form isn’t already enforced upstream, consider normalizing format_signature before lookup (e.g. lowercasing, stripping MIME prefixes, alias mapping) or clearly documenting the expected canonical format here.
Suggested implementation:
# Format signature -> structure-aware mutation operators that are almost
# certainly useful for that format (see core.mutations.FORMAT_MUTATIONS).
#
# The lookup below expects a *normalized* format signature. Call
# `_normalize_format_signature(profile.format_signature)` before using
# `_FORMAT_OPERATOR_HINTS` to avoid brittleness due to case, MIME-style
# prefixes (e.g. `image/png`), or common aliases (`jpg` vs `jpeg`,
# `riff/wav` vs `riff`, etc.).
def _normalize_format_signature(signature: str) -> str:
"""Normalize a format signature into the canonical key used in
`_FORMAT_OPERATOR_HINTS`.
This is intentionally conservative; it lowercases, strips common
MIME-style prefixes, and applies a small alias map for widely-used
variants. If a format is not recognized, the original (lowercased)
signature is returned, so adding new formats remains straightforward.
"""
sig = signature.strip().lower()
if not sig:
return sig
# Common alias mappings for well-known formats.
alias_map = {
# PNG
"png": "png",
"image/png": "png",
# JPEG
"jpeg": "jpeg",
"jpg": "jpeg",
"image/jpeg": "jpeg",
# RIFF container (e.g. WAV)
"riff": "riff",
"riff/wav": "riff",
"wav": "riff",
# Compression formats
"gzip": "gzip",
"application/gzip": "gzip",
"bz2": "bz2",
"bzip2": "bz2",
"xz": "xz",
"application/x-xz": "xz",
"zlib": "zlib",
}
# Direct alias match first.
if sig in alias_map:
return alias_map[sig]
# For MIME-like strings, prefer the subtype as the canonical key.
if "/" in sig:
main, subtype = sig.split("/", 1)
subtype = subtype.strip().lower()
if subtype in alias_map:
return alias_map[subtype]
# Fall back to subtype if we don't know it explicitly.
return subtype
return sig
_FORMAT_OPERATOR_HINTS: dict[str, tuple[str, ...]] = {
"png": ("png_chunk_mutate", "png_crc_fix"),
"jpeg": ("jpeg_chunk_mutate", "jpeg_crc_fix"),
"gzip": ("gzip_chunk_mutate",),
"bz2": ("gzip_chunk_mutate",),
"xz": ("gzip_chunk_mutate",),
"zlib": ("zlib_chunk_mutate",),
}- Wherever
_FORMAT_OPERATOR_HINTSis consulted (likely something like_FORMAT_OPERATOR_HINTS.get(profile.format_signature, ...)), update the lookup to normalize first, e.g.:- Replace
profile.format_signaturewith_normalize_format_signature(profile.format_signature)in the key used for_FORMAT_OPERATOR_HINTS.
- Replace
- If the profiler or
Profiletype has documentation/comments aboutformat_signature, update them to mention that signatures are normalized via_normalize_format_signatureand give examples of accepted forms (PNG,image/png,riff/wav,jpg).
| def init_arm(self, name: str, prior_alpha: float = 1.0, prior_beta: float = 1.0) -> None: | ||
| """Register a mutation operator arm with a Beta prior. | ||
|
|
||
| Defaults to the uninformative Beta(1, 1) prior. Callers with prior | ||
| knowledge about an operator's likely usefulness (e.g. static target | ||
| profiling indicating a specific file format) can pass a stronger | ||
| prior to bias early Thompson sampling before any evidence has been | ||
| observed. A no-op if the arm is already registered — the prior only | ||
| applies at first registration and is never overwritten by later | ||
| calls, matching the existing idempotent behavior of this method. |
There was a problem hiding this comment.
suggestion (bug_risk): Clarify the documented contract for prior parameters vs. the clamping behavior.
The docstring states prior_alpha/prior_beta "must be > 0", but the implementation clamps them with max(prior_*, MIN_BETA_PARAM). This lets callers pass 0 or negative values that are silently coerced to MIN_BETA_PARAM. Please either document this clamping behavior explicitly or consider raising/logging on non-positive inputs to surface configuration errors earlier.
Suggested implementation:
def init_arm(self, name: str, prior_alpha: float = 1.0, prior_beta: float = 1.0) -> None:
"""Register a mutation operator arm with a Beta prior.
Defaults to the uninformative Beta(1, 1) prior. Callers with prior
knowledge about an operator's likely usefulness (e.g. static target
profiling indicating a specific file format) can pass a stronger
prior to bias early Thompson sampling before any evidence has been
observed. A no-op if the arm is already registered — the prior only
applies at first registration and is never overwritten by later
calls, matching the existing idempotent behavior of this method.
The `prior_alpha` and `prior_beta` parameters are expected to be
strictly positive. For robustness, the implementation clamps
non‑positive values up to the internal minimum Beta parameter
`MIN_BETA_PARAM` via `max(prior_*, MIN_BETA_PARAM)`. This means that
passing 0 or negative values will not raise, but will instead behave
as if `prior_* == MIN_BETA_PARAM`.If elsewhere in this docstring (or in parameter docs not shown in the snippet) you explicitly state that prior_alpha and prior_beta "must be > 0" without mentioning clamping, update that text to reference the clamping behavior described here to keep the contract consistent.
| class TestFormatOperatorPriors: | ||
| def test_no_hints_when_profile_empty(self): | ||
| p = TargetProfile() | ||
| assert format_operator_priors(p) == {} | ||
|
|
||
| def test_png_format_boosts_png_operators(self): | ||
| p = TargetProfile() | ||
| p.format_signature = "png" | ||
| priors = format_operator_priors(p) | ||
| assert priors["png_chunk_mutate"] == (2.0, 1.0) |
There was a problem hiding this comment.
suggestion (testing): Add a test that interesting_strings alone also boost dictionary-aware operators
You currently test magic_bytes and boundary_markers boosting _DICT_OPERATORS, but not interesting_strings, which is also part of the condition. Please add a test like:
def test_interesting_strings_boost_dict_operators(self):
p = TargetProfile()
p.interesting_strings = [b"token"]
priors = format_operator_priors(p)
assert "dict_insert" in priors
assert "dict_append" in priorsto exercise the interesting_strings branch of if profile.magic_bytes or profile.boundary_markers or profile.interesting_strings:.
There was a problem hiding this comment.
Pull request overview
This PR seeds the Thompson-sampling operator scheduler (MonteCarloScheduler) with informative Beta priors derived from static target profiling, so operator selection can start biased toward format- and token-relevant mutations instead of always starting from Beta(1, 1).
Changes:
- Added
format_operator_priors()to derive per-operator(alpha, beta)overrides fromTargetProfileformat/token hints. - Extended
MonteCarloScheduler.init_arm()to accept optional Beta prior parameters (with clamping) and exposed asupports_priorscapability flag. - Wired the fuzzer’s arm registration to pass priors only to schedulers that advertise prior support; updated README and added unit tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_target_profiler.py | Adds unit tests for format_operator_priors() output and basic prior sanity checks. |
| tests/test_montecarlo.py | Adds unit tests verifying informative priors, idempotency, and clamping behavior in init_arm(). |
| src/fuzzer_tool/services/fuzzer.py | Passes derived operator priors into arm registration for schedulers that support priors (MC bandit). |
| src/fuzzer_tool/core/target_profiler.py | Introduces operator-prior derivation and format/operator hint tables used to seed the bandit. |
| src/fuzzer_tool/core/montecarlo.py | Updates the Thompson scheduler to accept per-arm Beta priors and clamps invalid parameters. |
| README.md | Documents the new informative-prior behavior under static target analysis. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _FORMAT_OPERATOR_HINTS: dict[str, tuple[str, ...]] = { | ||
| "png": ("png_chunk_mutate", "png_crc_fix"), | ||
| "jpeg": ("jpeg_chunk_mutate", "jpeg_crc_fix"), | ||
| "gzip": ("gzip_chunk_mutate",), | ||
| "bz2": ("gzip_chunk_mutate",), | ||
| "xz": ("gzip_chunk_mutate",), | ||
| "zlib": ("zlib_chunk_mutate",), | ||
| "riff": ("bmp_chunk_mutate",), | ||
| } |
| # Beta prior for operators the profile suggests are relevant: same total | ||
| # "pseudo-observation" mass as the uninformative default (1, 1), but shifted | ||
| # toward success so Thompson sampling favors them before real evidence | ||
| # arrives. Weak enough that a handful of real failures will correct it. |
Seed Thompson-sampling bandit with informative priors from static target profiling
The x86 instruction-stream decoder (_decode_insns) was the #1 CPU consumer (0.216s/1000 iters) on targets where x86 mutations are irrelevant (PNG fuzzing): gating the operator behind a CLI flag cuts throughput cost to a single _AVAILABLE predicate check. Follows the enable_regex_bomb template: argparse flag + Fuzzer kwarg + registry availability predicate; no changes to build_dispatch/build_ops/ _register_arms. Removes the arm hint from _FORMAT_OPERATOR_HINTS so the profiler no longer auto-suggests it. Throughput 526 -> 664 eps (+26%) on the 2000-iter PNG hotpath run.
Every mutation operator arm in the Thompson-sampling bandit (
MonteCarloScheduler) started from the same uninformative Beta(1, 1) prior, even when static analysis already knows the target's input format (e.g. PNG, gzip) or has extracted dictionary tokens — evidence the bandit would otherwise have to rediscover through executions.Bandit
MonteCarloScheduler.init_arm()accepts optionalprior_alpha/prior_beta; defaults unchanged (Beta(1,1))MIN_BETA_PARAMconstant to keepbetavariate()sampling stablesupports_priorscapability flag so only Bayesian schedulers accept priors (MOpt/Replicator/Elo use non-Beta internal state and are unaffected)Static profiling → priors
format_operator_priors()incore/target_profiler.py: derives{operator: (alpha, beta)}overridesFORMAT_MUTATIONSoperatorsDICT_MUTATIONSoperatorsWiring
services/fuzzer.py's_register_arms()computesformat_operator_priors(self._profile)and passes it through to the bandit only, with defensive validation before unpackingREADME updated to document the new prior-derivation behavior under Static Target Analysis.
Summary by Sourcery
Seed the Thompson-sampling scheduler with informative priors derived from static target profiling and wire these priors into scheduler arm registration.
New Features:
Enhancements:
Tests: