Skip to content

smite-ir-mutator: drop 5% fresh-generation branch in fuzz - #154

Open
erickcestari wants to merge 1 commit into
morehouse:masterfrom
erickcestari:remove-fresh-mutator-ir
Open

smite-ir-mutator: drop 5% fresh-generation branch in fuzz#154
erickcestari wants to merge 1 commit into
morehouse:masterfrom
erickcestari:remove-fresh-mutator-ir

Conversation

@erickcestari

Copy link
Copy Markdown
Contributor

AFL's corpus scheduler already handles seed rotation, and gen-insert supplies generator-driven novelty without discarding the seed. Fresh generation now only serves as a decode/empty-input fallback.

We don't need the fresh-generation branch anymore, since we already have gen-insert that inserts new program in an existing one.

AFL's corpus scheduler already handles seed rotation, and gen-insert
supplies generator-driven novelty without discarding the seed. Fresh
generation now only serves as a decode/empty-input fallback.
@Chand-ra

Copy link
Copy Markdown

I don't think GeneratorInsertionMutator is an apt replacement for the "fresh-generation branch" on its own, since it can only mutate the chosen seed and not produce a new one.

I can infer from your comment that this behaviour is supposed to be covered up by AFL++'s corpus scheduler, but that raises the question of why we did not rely on it in the first place? I believe it's because (again) the scheduler cannot generate new seeds, only rotate between the existing.

The fresh-generation branch encompasses a "drastically switch the protocol mechanism that we're testing right now" component that isn't fully captured by the gen-insert + corpus scheduler alternative. Whether that is a useful component for our fuzzer or not, is something I would definitely like to see some statistical data arguing for/against.

@erickcestari

Copy link
Copy Markdown
Contributor Author

I can infer from your comment that this behaviour is supposed to be covered up by AFL++'s corpus scheduler, but that raises the question of why we did not rely on it in the first place?

The fresh-generation branch was first added because we didn't have any mutator that adds new program into the input. So we needed a way to generate a valid program somewhere.

I believe it's because (again) the scheduler cannot generate new seeds, only rotate between the existing.

It rotate seeds, if it's not rotating what is the propose of having coverage-guided feedback to save inputs if the fuzzer will never use those? I've run smite with logs enabled and after initial stage it starts using new seeds.

Whether that is a useful component for our fuzzer or not, is something I would definitely like to see some statistical data arguing for/against.

Yes, I agree. I'll provide the statistical data as soon as possible. I'll start from an empty seed and use #141 as the base, since that enables deeper fuzzing.

@erickcestari

erickcestari commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@Chand-ra

Target: ldk

Median Coverage Over Time

image

Distribution Comparisons

Final Edge Coverage Area Under Curve (Speed)
Final Edge Coverage Area Under Curve (Speed)

CSV:

Target,Duration (h),n (Baseline),n (Exp.),Median Cov. (Baseline),Median Cov. (Exp.),IQR Cov. (Baseline),IQR Cov. (Exp.),Raw p-value (Cov.),Â12 (Cov.),Median AUC (Baseline),Median AUC (Exp.),IQR AUC (Baseline),IQR AUC (Exp.),Raw p-value (AUC),Â12 (AUC),Union Cov. (Baseline),Union Cov. (Exp.),Execs/s (Baseline),Execs/s (Exp.),Adj. p-value (Cov.),Sig_Cov,Adj. p-value (AUC),Sig_AUC
ldk,0.9977777777777778,20,20,15097.5,15105.5,93.0,184.25,1.0,0.50125,14754.63806494703,14777.781348379427,331.1292610134569,455.31221797619764,0.9031164959895642,0.5125,15639,15508,77.825,69.78,1.0,False,0.9031164959895642,False

I'll also try to test with longer runs of 6 hours.

@Chand-ra

Copy link
Copy Markdown

Thanks for running the evaluation experiments, @erickcestari!

Here's the CSV data you provided as a Markdown table:

Target Duration (h) n (Baseline) n (Exp.) Median Cov. (Baseline) Median Cov. (Exp.) IQR Cov. (Baseline) IQR Cov. (Exp.) Raw p-value (Cov.) Â12 (Cov.) Median AUC (Baseline) Median AUC (Exp.) IQR AUC (Baseline) IQR AUC (Exp.) Raw p-value (AUC) Â12 (AUC) Union Cov. (Baseline) Union Cov. (Exp.) Execs/s (Baseline) Execs/s (Exp.) Adj. p-value (Cov.) Sig_Cov Adj. p-value (AUC) Sig_AUC
ldk 0.9977777777777778 20 20 15097.5 15105.5 93.0 184.25 1.0 0.50125 14754.63806494703 14777.781348379427 331.1292610134569 455.31221797619764 0.9031164959895642 0.5125 15639 15508 77.825 69.78 1.0 False 0.9031164959895642 False

The A12 statistic is ~0.5 and the p-value for is >> 0.05 for both final coverage and AUC. This points to the fact that the configurations are identical in terms of coverage-capabilities. We might be able to get a stronger result if we increase the timeout/trial count, or change the target.

What is interesting here is the fact that the IQR spread of the experimental config (in terms of both final coverage and AUC) is significantly higher than the baseline. I understand why the execs/sec would be better for the baseline (completely new seeds every so often prevents getting stuck on stale inputs), but the variance in performance is surprising.

Asked an LLM for an opinion and this is what I got:

By entirely removing that 5% fresh generation (the experimental config), the mutator becomes strictly path-dependent on the lineage of the existing seeds.

  • The Good Runs: If a trial gets "lucky" early on—making a mutation that bypasses a complex check in ldk—it exploits that lineage deeply and achieves high coverage.

  • The Bad Runs: If a trial gets unlucky and the queue gets saturated with complex but unhelpful IR programs, it gets stuck in a local optimum. Without that 5% random kick to force a reset, it stays stuck longer.

This divergence between the "lucky" early-queue runs and the "unlucky" stuck runs is exactly what widens the IQR.

which seems reasonable.

I think an experiment with a a longer timeout is worth running here. Over a long enough timeline, the experimental config might eventually stall out entirely without fresh inputs, while the baseline continues to slowly discover new paths.

@morehouse

Copy link
Copy Markdown
Owner

This result matches my intuition. The 5% fresh-generation branch acts like a fixed seed corpus, helping to reduce variance in fuzzing. It also provides a bit of a weight pulling the fuzzer down to smaller programs. Without that branch, the main way to exercise new flows is to insert an entire generator sequence into an existing program, thus creating a larger combined program. In general, larger inputs reduce fuzzing efficiency, which is the whole point of AFL++'s trim operation.

BTW, I've noticed for many large programs, execution actually stops early on in the program. I think that's a hint that a truncating minimizer would be quite helpful for reducing average input size.

@Chand-ra

Copy link
Copy Markdown

BTW, I've noticed for many large programs, execution actually stops early on in the program. I think that's a hint that a truncating minimizer would be quite helpful for reducing average input size.

Truncating as in deleting parts of the program? Wouldn't that break the coverage-preserving invariant for minimizers? Also, I'm curious about how you observed program execution stopping early, maybe there's a different way to mitigate it if we can pinpoint the cause.

@morehouse

morehouse commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Truncating as in deleting parts of the program? Wouldn't that break the coverage-preserving invariant for minimizers?

No, it's fine if coverage changes sometimes -- AFL++ automatically filters such changes out. So we can have minimizers that do drastic things like delete half the program, and AFL++ will still do the right thing.

Suppose only the first 10 lines of a program are ever executed. A truncation minimizer would be very effective since we should be able to delete anything after the first 10 lines and the coverage will be unchanged.

Also, I'm curious about how you observed program execution stopping early, maybe there's a different way to mitigate it if we can pinpoint the cause.

There are many ways for execution to stop early. But by far the most common is receiving an error or warning message instead of the the message we expected.

@Chand-ra

Copy link
Copy Markdown

There are many ways for execution to stop early. But by far the most common is receiving an error or warning message instead of the the message we expected.

Oh okay, I think I misunderstood what you said initially. Execution stops on early in many large programs because we hit one of these expected error paths early on, making the rest of the program no-op. This is expected behavior and not something that happens because the program is "too large".

The truncating minimizer makes sense within this context then.

@morehouse

Copy link
Copy Markdown
Owner

Here's an 8-trial 15h run against LDK.

Configuration A (Baseline): baseline
Configuration B (Experimental): drop5

Target Duration (h) n (Baseline) n (Exp.) Median Cov. (Baseline) Median Cov. (Exp.) Adj. p-value (Cov.) Â12 (Cov.) Median AUC (Baseline) Median AUC (Exp.) Adj. p-value (AUC) Â12 (AUC) Union Cov. (Baseline) Union Cov. (Exp.) Execs/s (Baseline) Execs/s (Exp.)
ldk 15 8 8 22938 22968.5 0.505361 0.609375 302061 294055 0.95913 0.484375 25539 28915 127.205 106.035

Target: ldk

Median Coverage Over Time

ldk_time_series

Distribution Comparisons

Final Edge Coverage Area Under Curve (Speed)
ldk_boxplot ldk_auc_boxplot

@Chand-ra

Copy link
Copy Markdown

Here's an 8-trial 15h run against LDK.

Configuration A (Baseline): baseline Configuration B (Experimental): drop5

Target Duration (h) n (Baseline) n (Exp.) Median Cov. (Baseline) Median Cov. (Exp.) Adj. p-value (Cov.) Â12 (Cov.) Median AUC (Baseline) Median AUC (Exp.) Adj. p-value (AUC) Â12 (AUC) Union Cov. (Baseline) Union Cov. (Exp.) Execs/s (Baseline) Execs/s (Exp.)
ldk 15 8 8 22938 22968.5 0.505361 0.609375 302061 294055 0.95913 0.484375 25539 28915 127.205 106.035

Huh, turned out to be less favorable towards baseline than I expected (in terms of final coverage). execs/sec and AUC seem to validate our hypotheses though: the 5% branch acts as a tether that prevents corpus inputs from ballooning in size.

@erickcestari

Copy link
Copy Markdown
Contributor Author

I've ran a 20-trial 12h run against LDK. But since my machine is slower than the morehouse the results aren't so good. I'll try running a final and longer one to get a a better overview.

Configuration A (Baseline): baseline
Configuration B (Experimental): drop5

1. Summary Statistics

Target Duration (h) n (Baseline) n (Exp.) Median Cov. (Baseline) Median Cov. (Exp.) Adj. p-value (Cov.) Â12 (Cov.) Median AUC (Baseline) Median AUC (Exp.) Adj. p-value (AUC) Â12 (AUC) Union Cov. (Baseline) Union Cov. (Exp.) Execs/s (Baseline) Execs/s (Exp.)
ldk 11.9975 20 20 17170.5 16570 0.273263 0.3975 188588 185713 0.44075 0.4275 31111 30863 56.855 43.69

Target: ldk

Median Coverage Over Time

image

Distribution Comparisons

Final Edge Coverage Area Under Curve (Speed)
image image

@Chand-ra

Chand-ra commented Jul 28, 2026

Copy link
Copy Markdown

I've ran a 20-trial 12h run against LDK. But since my machine is slower than the morehouse the results aren't so good. I'll try running a final and longer one to get a a better overview.

Right. At this point, I don't think running more counts of short trials will give us a different picture. A smaller count of longer trials (~24 hours) is probably our best bet at that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants