fix(ci): silence bogus SA5011, portable repo root, and an SVG path parser hang - #19
Merged
avdoseferovic merged 2 commits intoJul 29, 2026
Merged
Conversation
The lint job reported 23 SA5011 nil dereferences, all in _test.go files and all
of the same shape: `if x == nil { t.Fatal(...) }` guarding the dereference below
it. The guard is correct; SA5011 needs staticcheck's "never returns" fact for
testing.common.Fatal to see it, and golangci-lint does not carry that fact into
a linted package reliably. It is nondeterministic — 23 reports on the runner,
none locally with a cold cache, and toggling any unrelated analyzer flips the
outcome. There is nothing to fix in the test code, so SA5011 is excluded in
tests, where a real nil dereference fails loudly with a panic and a stack trace
anyway. Non-test code, which never calls t.Fatal, keeps the check.
That also retires the reason testableexamples was held back, so it is enabled
now; every example already carries its `// Output:` marker, so it reports
nothing.
The Windows job failed TestCache_LoadImage because buildPath derived the
repository root by stripping the literal "internal/cache" from os.Getwd(). The
working directory is backslash-separated there, so the replace matched nothing
and the path pointed into the package directory. It now joins relative to the
package with filepath, the way the font tests already resolve assets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FuzzRasterizeWithLimit hung on `<svg><pAth d="Z0 0L10 10C1 2 3 4 5 6Z"/></svg>`. Z is the only path command that consumes no tokens, and readCommand lets a number token repeat the command in effect, so a number after Z re-applied Z without ever advancing the cursor. Where the Z had a current point the loop also appended to path.ops on every pass, so the spin came with unbounded memory growth. SVG path data reaches this parser from <img src> content in untrusted documents, which makes it a denial of service rather than a cosmetic bug. Closepath takes no operands, so a number following one cannot be an implicit repeat of it: the path is malformed and is now rejected, which is how this parser already treats every other grammar violation. parse also checks that each iteration advanced, so any future zero-operand command rejects the path instead of spinning. The crashing input is kept as a seed corpus entry, so plain `go test ./pkg/svg/` replays it from now on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
avdoseferovic
deleted the
fix/ci-sa5011-false-positives-and-windows-paths
branch
July 29, 2026 15:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Three CI failures, unrelated to each other.
Lint: 23 bogus SA5011 nil dereferences. All of them are in
_test.gofiles and all have the same shape —if x == nil { t.Fatal(...) }guarding the dereference below it. The guard is correct. SA5011 needs staticcheck's "never returns" fact fortesting.common.Fatalto see it, and golangci-lint does not carry that fact into a linted package reliably. It is nondeterministic: 23 reports on the runner, none locally with a cold cache (golangci-lint cache clean, also tried underGOMAXPROCS=4 --concurrency=4to mimic the runner), and toggling any unrelated analyzer flips the outcome. That flakiness was already documented in.golangci.ymlas the reasontestableexampleswas held back.There is nothing to fix in the test code, so SA5011 is excluded in tests only, with the rationale recorded next to the rule. A real nil dereference in a test fails loudly with a panic and a stack trace, so little is lost; non-test code, which never calls
t.Fatal, keeps the check. Enabling the exclusion retires the reasontestableexampleswas off, so it is enabled here — every example already carries its// Output:marker, so it reports nothing.Windows:
TestCache_LoadImage.buildPathderived the repository root withstrings.ReplaceAll(dir, "internal/cache", "")overos.Getwd(), then joined withpath.Join. The working directory is backslash-separated on Windows, so the replace matched nothing and the path becameD:\a\paper\paper\internal\cache/test/assets/images/biplane.jpg. It now joins relative to the package directory withfilepath, the wayinternal/pdf/font_pdf_test.goalready resolves assets. This was the only instance of that pattern in the root module.Fuzz:
FuzzRasterizeWithLimithung on<svg><pAth d="Z0 0L10 10C1 2 3 4 5 6Z"/></svg>.Zis the only path command that consumes no tokens, andreadCommandlets a number token repeat the command in effect, so a number afterZre-appliedZwithout ever advancing the cursor. Where theZhad a current point, the loop also appended topath.opson every pass, so the spin came with unbounded memory growth. Path data reaches this parser from<img src>content in untrusted documents, so this is a denial of service, not a cosmetic bug.Closepath takes no operands, so a number following one cannot be an implicit repeat of it — the path is malformed and is now rejected, which is how this parser already treats every other grammar violation.
parseadditionally checks that each iteration advanced, so any future zero-operand command rejects the path instead of spinning. The crashing input is committed as a seed corpus entry, so plaingo test ./pkg/svg/replays it from now on.Where should the reviewer start
internal/svg/pathdata.go— thereadCommandguard is the fix; the progress check inparseis the structural backstop.internal/svg/pathdata_test.go— two new cases in the existing malformed-input table; both hang without the fix..golangci.yml— the new exclusion rule and its rationale, plustestableexamplesmoving into the enabled list.internal/cache/cache_test.go—buildPathis now a one-linefilepath.Join; the comment records what it used to do and why that broke.Verification
The first two fixes are confirmed by CI on this branch: the
lintandtest-windowsjobs both passed on the commit before this one, which is what the earlier run of this PR was for.For the parser fix: the two new table cases time out without it and pass with it.
FuzzRasterizeWithLimitthen ran 12.6M executions clean over two minutes, at 130–230k execs/sec against roughly 20k/sec in the failing CI run — the stall showing up in the throughput. All five fuzz targets were then run at the same 60s budget CI uses (pkg/readerFuzzParse,pkg/svgFuzzParseBytes and FuzzRasterizeWithLimit,pkg/html/cssFuzzParseValues,pkg/htmlFuzzFromString) and all pass.make dodpasses end to end (build, test, fmt, lint — no files reformatted).The SA5011 exclusion was verified to match rather than assumed: a throwaway test file that genuinely triggers SA5011 reported 2 issues without the rule — the primary report and its
SA5011(related information)line — and 0 with it. That probe file is not part of this PR.Notes
The Windows fix is confirmed by CI, but the job stays
continue-on-error: true; making it blocking is a separate call.Because the SA5011 flake does not reproduce locally, the exclusion is verified against a synthetic SA5011 report rather than against the CI-only ones.
Fuzzing is a search, so a clean run is evidence rather than proof — later runs may surface other findings.
Related Issue
None.
Checklist
make dodwith no issues🤖 Generated with Claude Code