How the maintainers of modernc.org/sqlite respond when something goes wrong: a
confirmed vulnerability, a bad release, or a compromised account.
SECURITY.md is the public-facing document -- how to report, what to expect, what is in scope. This one is the procedure we follow afterwards. It is published for the same reason Bootstrap publishes theirs: a plan nobody can read is hard to trust. Operational detail that would help an attacker -- the credential inventory, where each token lives, how to revoke it -- is kept in the maintainers' private notes, not here.
- A confirmed vulnerability in code we ship: the hand-written driver, the transpiled
SQLite in
lib/,sqlite-vecinvec/, or the VFS bridge invfs/. - A vulnerability in SQLite itself, which reaches users through this module rather than through their system packages.
- A released version that is broken, wrong, or malicious.
- Compromise of a maintainer account, an access token, or the push mirror.
- A secret committed to the repository or exposed in logs.
- A builder result that cannot be trusted: a target reported green that did not build or pass. The builder dashboard is what a release is tagged on, so a false green is a release decision made on bad data. Every tag made on it is suspect until the affected targets are re-run; if one then fails, Phase 5 applies.
An ordinary bug, however severe, is not an incident. It goes through the normal issue and release process.
There are two maintainers, roughly six hours apart (Central European and UTC+8). Whoever picks the report up first says so explicitly in the thread and runs it until they hand over. That one sentence prevents the common failure of a two-person team: both people investigating, or each assuming the other has.
- Acknowledge the reporter. The commitment in
SECURITY.mdis seven days; usually it is much sooner. - Reproduce it. If there is no proof of concept, write one. If it cannot be reproduced, say so to the reporter and ask for specifics rather than closing.
- Decide whether it is a vulnerability at all, and whether it is ours. See the scope
section of
SECURITY.md. - Assess severity: what an attacker gets, and what they need to already control -- hostile SQL, a hostile database file, or only hostile data in a table.
- Open a draft GitHub Security Advisory to hold the work. Private vulnerability reporting is enabled, so a report that arrives that way already has one.
Answer these before writing any fix; they determine who fixes what, and where.
- Which layer? Hand-written Go, generated Go, or SQLite's own C. This decides the entire remediation path, and the layers are not equally ours to fix.
- Which targets? The module supports 20
GOOS/GOARCHcombinations and the generated code differs per target. A fault may exist on some and not others. - Which versions? When was it introduced, and is the current release affected.
For generated code,
git show vX.Y.Z:vendor.jsonnames thelibsqlite3andlibsqlite_veccommits a release was vendored from. - Does the fix move
modernc.org/libc? If so it cannot ship from this repository alone; see issue #177.
The path depends on the layer, and only the first is quick:
| Layer | Path |
|---|---|
Hand-written Go (driver, vtab/, pcache/, vfs/ Go side) |
Fix on GitLab, release. |
Generated Go in lib/, vec/ |
The fix belongs in modernc.org/libsqlite3 (and modernc.org/libsqlite_vec for vec/), which own the transpilation and the patch set. Re-transpile there, make vendor here, make build_all_targets, release. |
Generated Go in vfs/ |
The C is vfs/c/vfs.c, in this repository. Fix the C and re-transpile it here; each generated file's first line records the ccgo command that produced it. |
| SQLite's own C | Report upstream at sqlite.org. Upstream owns the fix; we own shipping it. If the wait is unacceptable, a local patch can go into the libsqlite3 patch set and be dropped when upstream lands theirs -- there is precedent in the v1.56.0 journal-rollback fix. |
When the fix moves modernc.org/libc, it is four releases in one incident, in
this order, and none can be skipped or reordered, because each pins the one before:
modernc.org/libc, released with the fix.modernc.org/libsqlite3, itsgo.modbumped to that libc, re-transpiled, tagged.modernc.org/libsqlite_vec, bumped to both, re-transpiled, tagged.- Here: both sibling checkouts at those tags,
go.modon the new libc,make vendor, the builders green on all 20 targets, then the tag.make vendorrefuses a dirty checkout, two checkouts on different libc versions, and alibsqlite_vecthat requires anotherlibsqlite3commit than../libsqlite3is at, and records what it used invendor.json. The test suite fails when the libc ingo.moddiffers from the one invendor.json, so thego.modbump goes in the same push asmake vendor, or every builder is red until it does.
lib/ and vec/ must come from the same libc. Do not re-vendor one while the other
stays on the old version.
Four rules that do not bend under time pressure:
- Never merge a pull request on the GitHub mirror. GitHub
masterwould gain a commit GitLab does not have, and because the mirror keeps divergent refs and force-push is disabled, every later mirror push fails. Cross-merge into GitLab by hand, asHACKING.mddescribes. - Do not bump
modernc.org/libcon its own. The generated code is pinned to the exact versiongo.modnames. - Tag manually, and only when the builders are green.
builder.jsonsets"autotag": "<none>"deliberately. A fix that breaks three platforms is a second incident. - Run
make sbomafter any dependency change. CI fails on stale documents, and an SBOM that misstates what shipped is worse than none during an incident.
As SECURITY.md states: a GitHub Security Advisory, then an entry in the
Go vulnerability database so govulncheck
reports it, then the release note. The middle step is the one that actually reaches
Go developers and is part of shipping the fix, not an afterthought.
Credit the reporter by name unless they prefer otherwise.
A published Go module version cannot be recalled. This is the fact to have internalised before the day it matters. From the module mirror's own FAQ:
Whenever possible, the mirror aims to cache content in order to avoid breaking builds for people that depend on your package, so this bad release may still be available in the mirror even if it is not available at the origin. The same situation applies if you delete your entire repository.
Deleting the tag does nothing. Deleting the repository does nothing. The checksum database has it permanently.
What actually works:
- Release a fixed version.
- Add a
retractdirective togo.modnaming the bad version or range, with a comment saying why, and release that. Retraction is advisory -- the version stays downloadable -- butgo getstops selecting it andgo list -m -versionsstops offering it. - Say so in
CHANGELOG.md.
The retract block in go.mod already carries seven entries. It is load-bearing
history, not clutter: never remove entries while editing it.
Assume the worst ordering: revoke first, investigate second.
- Revoke before diagnosing. Sessions, then tokens, then keys. A token that might be compromised is compromised. The mirror push credential is a case of its own. It can rewrite what GitHub users see while GitLab looks fine, so nothing on the canonical side reveals its misuse. Revoke it with the rest, and compare the mirror's branches and tags against GitLab's rather than assuming they match.
- Check what was done with it, not only what it could do: recent commits on both hosts, tags created, releases published, mirror settings, workflow changes, and whether any release artifact changed after it was tagged.
- Re-establish the account: authentication factors, recovery codes, and confirm no additional factor or key was added by someone else.
- Assume any release made during the window is suspect and treat it under Phase 5, which is the only reason Phase 5 exists in this document.
The specific credential inventory and revocation URLs live in the maintainers' private notes, deliberately not here.
Write down what happened and what was slow, while it is still annoying. If the
response revealed a gap, fix the document that should have prevented it --
SECURITY.md, CONTRIBUTING.md, HACKING.md, or this plan. A post-incident note
that changes no document has not finished.