.question
One CVE record in eight links a commit. Leave out the Linux kernel and it is one in twelve.
When a vulnerability is fixed in open source code, the quickest way to see what was wrong is to read the commit that fixed it. The obvious place to look for that commit is the CVE record. So we counted how often the record names a commit at all. Of 366,751 published records in the official CVE list, 45,067 link to a specific git commit. A third of those come from one numbering authority, the Linux kernel, which links a commit in every one of its 15,002 records. For everyone else it is 8.5 per cent.
A commit link is not proof of a fix. It might point at the fix, a backport of it, a test, or the change that introduced the bug. This page counts links. It does not judge what they point at.
.inputs
Two public files, pinned by hash.
The CVE Program publishes the whole list as a bulk export several times a day. We used the export taken at midnight UTC on 27 September 2026, and counted only records published before 1 September, so every record had at least 26 days to pick up its references. The list of exploited vulnerabilities comes from CISA's own data repository, pinned to one commit.
Anyone can fetch the same two files and get the same bytes. If your hashes differ from these, stop, because nothing further down will match.
$ curl -sSLO https://github.com/CVEProject/cvelistV5/releases/download/cve_2026-09-27_1600Z/2026-09-27_all_CVEs_at_midnight.zip.zip $ shasum -a 256 2026-09-27_all_CVEs_at_midnight.zip.zip 14223ccfa58a313d322aaae3358d83795ed1e314709055aafd047ae06c4e073a 2026-09-27_all_CVEs_at_midnight.zip.zip $ python3 -c "import os; print(os.path.getsize('2026-09-27_all_CVEs_at_midnight.zip.zip'))" 614745363 $ unzip -q 2026-09-27_all_CVEs_at_midnight.zip.zip && shasum -a 256 cves.zip c3cf4a7fb4cff00da44d9ed87b780a14e720d788d366e0ad1d9e41d10595f5ea cves.zip $ curl -sSL -o kev.json https://raw.githubusercontent.com/cisagov/kev-data/9dfdff658ee0fbbca1c648235a29a5402f6533ca/known_exploited_vulnerabilities.json $ shasum -a 256 kev.json 8a41e76a385f304a9f436f33640ed6bec1ca94d9dee699a4e9ecb908c9d51395 kev.json $ python3 -c "import json; d = json.load(open('kev.json')); print(d['catalogVersion'], d['count'])" 2026.09.25 1726
The first hash matches the digest GitHub publishes for that release asset. cves.zip inside the export is the file the script reads, and it is left zipped.
.method
What counts as a commit link.
A CVE record carries a list of reference URLs. We read the list written by the organisation that assigned the number, the CNA, and ask one question of each URL: does it name one specific git revision? The rules are small enough to print in full.
- counted
- Commit pages on GitHub, GitLab, Bitbucket and SourceForge, commits inside a GitHub pull request's commit list,
git.kernel.organdkernel.dancelinks, Google'sgooglesource.com, and any other URL that contains the word commit and a hash of 7 to 40 hex characters, which catches most cgit, gitweb and Gitea commit views. - not counted
- Pull requests and GitLab merge requests, Gerrit changes, compare views, file views pinned to a revision, gitweb and cgit views that never use the word commit, Subversion changesets, Mercurial revisions, advisories, mailing list posts and bug trackers. Pull requests, Subversion and Mercurial are counted separately further down, and the git shapes the rules skip are counted in the check section.
- scope
- Records in state PUBLISHED with a publication date before 1 September 2026. Rejected records are left out. The year used below is the year the record was published, not the year in its ID.
- not read
- References added by others. The ADP containers, where CISA and the CVE Program itself add data after publication, are checked once and add 32 records; that figure is reported and then ignored. NVD's website shows references of its own, and those are not in the CVE list, so they play no part here. Nor do commit hashes a record may carry as version bounds in its affected-products data.
$ sed -n '15,33p;49,56p' fixlinks.py SHA = r"[0-9a-f]{7,40}(?![0-9a-z])" GIT = re.compile("|".join([ r"github\.com/[^/]+/[^/]+/(?:pull/\d+/)?commits?/" + SHA, r"/-/commits?/" + SHA, # GitLab, any host r"gitlab[^/]*/.*/commit/" + SHA, r"git\.kernel\.org/.*(?:/c/|/linus/|[?&;]id=)" + SHA, r"kernel\.dance/#[0-9a-f]{12,40}", r"googlesource\.com/.*/\+/" + SHA, r"bitbucket\.org/.*/commits?/" + SHA, r"sourceforge\.net/.*/ci/" + SHA, ]), re.I) # cgit, gitweb, Gitea and similar: the word commit plus a hash parameter # or path segment. The hash must contain a letter so that numeric ids # such as ?id=1234567 are not mistaken for revisions. GENERIC_HASH = re.compile(r"[?&;/=](?:id=|h=)?(?=[0-9]*[a-f])" + SHA, re.I) NOT_VCS = re.compile(r"lists\.|mail|thread|bugzilla|issues?/", re.I) SVN = re.compile(r"/changeset/\d+|/changeset\?|[?&]rev=\d+(?!\d)", re.I) HG = re.compile(r"/rev/[0-9a-f]{12,40}(?![0-9a-z])", re.I) PULL = re.compile(r"github\.com/[^/]+/[^/]+/pull/\d+", re.I) def is_git(url): u = url.replace("%3B", ";").replace("%3b", ";") if not re.match(r"https?://", u, re.I): return False if GIT.search(u): return True return bool(re.search("commit", u, re.I) and GENERIC_HASH.search(u) and not NOT_VCS.search(u))
The script is fixlinks.py, Python 3.10 with no dependencies outside the standard library. Each section below is one run of it. Check you have the same file before you start.
$ shasum -a 256 fixlinks.py fixlinks-check.tsv 92d5b8aa2a2ef2b428ad0a43792cf9787ddb9b64a10d766bb8e6bedf2bfa98ef fixlinks.py 6b0200efd331a2fb230ad78c8386779633f471c19b2eeee0ad10d271c2c18fcc fixlinks-check.tsv
.count
The count.
$ python3 fixlinks.py cves.zip kev.json headline records in export {'PUBLISHED': 379989, 'REJECTED': 18412} published before 2026-09-01 366,751 link a git commit 45,067 12.3% plus ADP only 32 Linux kernel CNA 15,002 of 15,002 100.0% (33.3% of all linking records) everyone else 30,065 of 351,749 8.5%
The export holds 398,401 records. 366,751 are in scope. 45,067 of them, 12.3 per cent, carry at least one commit link in the CNA's own references.
The Linux kernel's CNA has published 15,002 records in scope and linked a commit in every one. The kernel only issues a CVE once a fix is in a stable tree, and the record is built around that commit. It makes the kernel a third of every commit-linking record in the list.
Take the kernel out and the picture changes: 30,065 of 351,749 records, 8.5 per cent. For most of the list, the record does not link a commit at all.
.years
Year by year.
Git was first released in April 2005, so the near-empty early years are expected. The rate climbs to 10.5 per cent in 2012, slips to 7.4 per cent in 2015, peaks at 17.0 per cent in 2016 and falls back to 6.3 per cent by 2019. After that the two columns tell different stories. With the kernel, the rate reaches 22.0 per cent in 2026. Without it, the rate fell from 11.8 per cent in 2022 to 6.6 per cent in 2024 and only jumped in 2026. The columns part company in 2024, the first year kernel records appear.
$ python3 fixlinks.py cves.zip kev.json years year published linked % excl. kernel linked % 1999 320 0 0.0% 320 0 0.0% 2000 1,437 1 0.1% 1,437 1 0.1% 2001 1,323 0 0.0% 1,323 0 0.0% 2002 1,690 0 0.0% 1,690 0 0.0% 2003 1,223 0 0.0% 1,223 0 0.0% 2004 1,612 0 0.0% 1,612 0 0.0% 2005 6,708 23 0.3% 6,708 23 0.3% 2006 6,885 45 0.7% 6,885 45 0.7% 2007 7,211 25 0.3% 7,211 25 0.3% 2008 5,673 72 1.3% 5,673 72 1.3% 2009 5,732 139 2.4% 5,732 139 2.4% 2010 4,639 194 4.2% 4,639 194 4.2% 2011 4,150 222 5.3% 4,150 222 5.3% 2012 5,288 557 10.5% 5,288 557 10.5% 2013 5,187 514 9.9% 5,187 514 9.9% 2014 7,928 610 7.7% 7,928 610 7.7% 2015 6,494 481 7.4% 6,494 481 7.4% 2016 6,457 1,098 17.0% 6,457 1,098 17.0% 2017 14,642 1,686 11.5% 14,642 1,686 11.5% 2018 16,510 1,349 8.2% 16,510 1,349 8.2% 2019 17,308 1,090 6.3% 17,308 1,090 6.3% 2020 18,363 1,211 6.6% 18,363 1,211 6.6% 2021 20,177 1,727 8.6% 20,177 1,727 8.6% 2022 25,000 2,940 11.8% 25,000 2,940 11.8% 2023 28,847 2,984 10.3% 28,847 2,984 10.3% 2024 39,924 6,631 16.6% 35,637 2,344 6.6% 2025 48,153 8,709 18.1% 42,478 3,034 7.1% 2026 57,870 12,759 22.0% 52,830 7,719 14.6%
Share of published records that link a git commit, by year of publication, 1999 to 2026. The 2026 figure covers January to August only.
Who moved the line.
In 2016 the Android CNA linked commits in 321 records. In 2017 MITRE linked commits in 1,285 records, but the list more than doubled in size, so the rate fell. In 2021 GitHub's CNA overtook MITRE, in 2022 and 2023 GitHub's CNA and huntr led, and from 2024 the kernel dominates.
$ python3 fixlinks.py cves.zip kev.json cnas CNA linked of records % Linux 15,002 15,002 100.0% GitHub_M 10,834 19,103 56.7% mitre 7,670 114,631 6.7% redhat 2,537 11,469 22.1% @huntrdev 1,579 1,582 99.8% VulDB 1,486 16,058 9.3% VulnCheck 1,266 6,333 20.0% google_android 934 5,140 18.2% @huntr_ai 437 803 54.4% snyk 397 868 45.7% EEF 224 227 98.7% debian 200 645 31.0% qualcomm 179 3,044 5.9% Chrome 178 5,943 3.0% CPANSec 134 303 44.2% year top three CNAs by linking records 2010 redhat 138, mitre 47, canonical 6 2011 redhat 149, mitre 57, certcc 6 2012 redhat 434, mitre 72, Chrome 38 2013 redhat 309, mitre 133, Chrome 63 2014 mitre 335, redhat 223, debian 19 2015 mitre 280, redhat 117, Chrome 29 2016 mitre 456, google_android 321, redhat 194 2017 mitre 1,285, redhat 115, google_android 89 2018 mitre 884, qualcomm 166, redhat 134 2019 mitre 915, redhat 41, dwf 34 2020 mitre 639, GitHub_M 367, snyk 68 2021 GitHub_M 691, mitre 621, @huntrdev 167 2022 @huntrdev 849, GitHub_M 819, mitre 468 2023 GitHub_M 1,029, @huntrdev 563, VulDB 465 2024 Linux 4,287, GitHub_M 1,377, mitre 323 2025 Linux 5,675, GitHub_M 1,857, mitre 285 2026 Linux 5,040, GitHub_M 4,677, VulnCheck 1,216 year top three CNAs by records published (of which linked) 2010 mitre 2,831 (47), redhat 464 (138), apple 265 (2) 2011 mitre 2,086 (57), redhat 515 (149), oracle 307 (0) 2012 mitre 2,107 (72), redhat 1,156 (434), oracle 406 (0) 2013 mitre 1,234 (133), redhat 970 (309), oracle 525 (0) 2014 mitre 2,655 (335), certcc 1,536 (8), redhat 950 (223) 2015 mitre 2,031 (280), apple 612 (0), microsoft 526 (0) 2016 mitre 1,647 (456), oracle 667 (1), adobe 547 (0) 2017 mitre 6,611 (1,285), oracle 869 (0), google_android 702 (89) 2018 mitre 7,290 (884), oracle 707 (0), microsoft 703 (0) 2019 mitre 7,730 (915), microsoft 845 (0), adobe 704 (0) 2020 mitre 7,221 (639), microsoft 1,262 (0), oracle 811 (0) 2021 mitre 6,473 (621), GitHub_M 1,065 (691), microsoft 871 (0) 2022 mitre 7,612 (468), GitHub_M 1,293 (819), WPScan 1,144 (1) 2023 mitre 6,237 (454), Patchstack 2,146 (0), VulDB 1,931 (465) 2024 mitre 5,910 (323), Patchstack 4,564 (0), Linux 4,287 (4,287) 2025 Patchstack 7,006 (0), VulDB 5,901 (201), Linux 5,675 (5,675) 2026 GitHub_M 9,464 (4,677), VulnCheck 5,212 (1,216), Linux 5,040 (5,040)
Patchstack, a WordPress security CNA, published 4,564 records in 2024 and 7,006 in 2025, and none of them links a commit. In 2026 GitHub's CNA published 9,464 records and linked a commit in 4,677. CNA names are the short names in each record's metadata, printed as the list spells them. @huntrdev and @huntr_ai are the same CNA, the huntr bug bounty platform, under an old and a new short name.
.who
It depends on who wrote the record.
The table above is the finding that matters most. Whether a record links a commit depends far more on who wrote it than on what it describes. The kernel links in 100 per cent of records, the Erlang Ecosystem Foundation in 98.7 per cent, and huntr in 99.8 per cent under its old name and 54.4 per cent under its new one. GitHub's CNA, which assigns numbers to maintainers who ask for them through GitHub security advisories, manages 56.7 per cent. MITRE, which assigns numbers for anything no other CNA covers, has 114,631 records in scope going back to 1999, years before git existed, and links a commit in 6.7 per cent of them. Chrome's CNA links a commit in 3.0 per cent.
This says how CVE records are written, and nothing about how well any of these projects fix their bugs. A project can ship a clean, well-described fix and still publish a record that points only at a release note.
.hosts
Where the commits live.
By record, GitHub carries the most. By link, the kernel's own git server carries far more, because kernel records usually link one commit for each stable branch the change was backported to, plus the mainline commit. The third row, other, is every remaining host: mostly project-run cgit and gitweb servers, plus Bitbucket and SourceForge.
$ python3 fixlinks.py cves.zip kev.json hosts host records links github.com 24,606 29,538 git.kernel.org 16,893 79,171 other 3,349 4,028 googlesource.com 892 1,071 other GitLab 219 250 gitlab.com 182 212
A record that links two hosts counts once under each. kernel.dance is folded into git.kernel.org. More records point at git.kernel.org (16,893) than the kernel CNA published, because other CNAs link commits on that server too, and it hosts more projects than the kernel.
.near-misses
Records that nearly link a commit.
A strict definition hides some useful links, so the script counts the common near misses separately, among records with no git commit link at all.
$ python3 fixlinks.py cves.zip kev.json shapes SVN changeset, no git commit 7,780 of which trac.wordpress.org 7,417 Mercurial revision, no git commit 314 GitHub pull request, no commit 4,297 no references at all 0 linking records, commit ref tagged 'patch' 3,676 of 45,067 linking records, 2+ distinct commits 19,747
- 7,417 WordPress records WordPress plugins and themes are still distributed from Subversion repositories, so their records link a changeset on WordPress's Trac server rather than a git commit. It is a real revision, just not a git one.
- 4,297 pull requests These records link a GitHub pull request but no commit. The commits are usually reachable from the pull request, but the record does not say which one it means, and a pull request can be rebased or squashed after the link was written.
-
3,676 tagged as a patch
CVE references can carry a
patchtag. Of the 45,067 records with a commit link, only 3,676 tag that link as a patch. Anything that filters on the CVE list's own tag sees a small fraction of the links that exist. NVD applies tags of its own, which are not counted here. - 19,747 with several Records linking two or more distinct commits. Kernel records, with one link per stable branch, are part of this. Elsewhere it can mean a change in several parts, or one change plus its backports. The script does not tell these apart.
.exploited
Exploited, and rarely linked.
CISA's Known Exploited Vulnerabilities catalogue is the list of CVEs with evidence of exploitation in the wild. Of its entries in scope, 5.4 per cent link a commit. That is lower than the list as a whole, which is what the catalogue's make-up predicts.
$ python3 fixlinks.py cves.zip kev.json kev KEV entries in scope 1,704 (catalogue lists 1,726) link a git commit 92 5.4% excluding kernel 81 of 1,693 KEV entries published on or after 2026-09-01 22
The last line accounts for the rest of the catalogue: 22 entries were published after the cut-off.
$ python3 -c "import json,collections; v=json.load(open('kev.json'))['vulnerabilities']; c=collections.Counter(x['vendorProject'] for x in v); print(len(v), 'entries,', len(c), 'vendors'); [print(f'{n:5} {k}') for k, n in c.most_common(10)]" 1726 entries, 283 vendors 389 Microsoft 99 Cisco 94 Apple 82 Adobe 75 Google 46 Oracle 40 Apache 35 Ivanti 31 Linux 30 Fortinet
The catalogue's four largest vendors, Microsoft, Cisco, Apple and Adobe, account for 664 of its 1,726 entries, and all four ship closed-source products. There is no public commit for a Windows or iOS fix, so a record cannot link one. The low rate most likely reflects what gets exploited rather than how the records are written. This page does not test that.
.check
Checking the script against the data.
A count built on regular expressions is only as good as the expressions. So the script draws its own seeded random sample: 200 URLs it counted as commit links, 200 URLs it rejected that still look like version control, and 100 whole records in which it found no commit link at all. Each line was then classified twice, in two separate runs of an automated reviewer, a language model, working from the URLs alone. The runs were not blind: both knew which of the three groups a line came from. Both verdicts are in the file for every line, so any of them can be checked by hand.
$ python3 fixlinks.py cves.zip kev.json sample | diff - <(grep -v '^#' fixlinks-check.tsv | tail -n +2 | cut -f2,3,6) && echo identical identical $ awk -F'\t' '!/^#/ && $1!="line" && $4!=$5' fixlinks-check.tsv | wc -l | tr -d ' ' 0 $ grep -v '^#' fixlinks-check.tsv | tail -n +2 | cut -f2,4 | sort | uniq -c 200 MATCH COMMIT 1 NEAR COMMIT 198 NEAR NOT_COMMIT 1 NEAR UNSURE 100 RECORD NOT_COMMIT
The kind, CVE and URL columns of the file are the script's own sample, line for line, and the two runs disagree on no line. All 200 counted links are commit links. Of the 200 rejected look-alikes, one was a commit link the script missed and one could not be decided. None of the 100 records with no link turned out to have one. The full file is fixlinks-check.tsv. Most lines take a second to check yourself.
$ awk -F'\t' '$2=="NEAR" && $4!="NOT_COMMIT"' fixlinks-check.tsv 291 NEAR CVE-2015-4469 UNSURE UNSURE http://anonscm.debian.org/cgit/collab-maint/libmspack.git/diff/debian/patches/fix-name-field-boundaries.patch?id=a25bb144795e526748b57884daf365732c7e2295 324 NEAR CVE-2021-3407 COMMIT COMMIT http://git.ghostscript.com/?p=mupdf.git%3Bh=cee7cefc610d42fd383b3c80c12cbc675443176a
The miss is a gitweb URL that names a commit with h= and never uses the word commit. The undecided one is a cgit diff restricted to a single file at one revision: arguably a commit view, arguably a file view. The next pane stops sampling and counts every shape like these across the whole export.
$ python3 fixlinks.py cves.zip kev.json misses git shapes the rules skip, in records with no counted link gitweb h= with no 'commit' 165 cgit patch or diff view 17 Pagure commit 14 commit in a PR file view 26 compare between two hashes 48 any of these 270 records counted only through a shape that is not a commit gist, user name contains commits 5 Gerrit change number read as hash 2 gitweb search page 1 any of these 8 headline as printed 12.3% with both corrections 12.4%
The sample found one miss. This counts where misses occur. Every record carrying one of five git shapes the rules skip was counted, as was every record counted only through something that is not a commit: a gist whose owner's name contains the word commits, a Gerrit change number read as a hash, a gitweb search page. Correcting for both moves the headline from 12.3 to 12.4 per cent. The compare views are arguable, since a range is not one commit. Leave out their 48 records and the corrected figure rounds back to 12.3 per cent, so either way the headline stands.
$ python3 fixlinks.py cves.zip kev.json pools references counted as commit links 114,270 references that look like VCS, not counted 63,374 records with no commit link 321,684 $ python3 -c " from math import sqrt def w(k, n, z=1.96): p = k / n; c = p + z*z/(2*n); s = z*sqrt(p*(1-p)/n + z*z/(4*n*n)); d = 1 + z*z/n return (c - s) / d, (c + s) / d lo, hi = w(200, 200); print(f'counted links that are commit links 200/200 {lo:.1%} to {hi:.1%}') lo, rec = w(0, 100); print(f'no-link records hiding a link 0/100 {lo:.1%} to {rec:.1%}') lo, hi = w(1, 200); print(f'look-alikes that are commit links 1/200 {lo:.2%} to {hi:.2%}') lo, hi2 = w(2, 200); print(f' counting the undecided one as a miss 2/200 {lo:.2%} to {hi2:.2%}') print(f'headline at the top of the look-alike interval {(45067 + 63374*hi2) / 366751:.1%}') print(f'headline at the top of the record interval {(45067 + 321684*rec) / 366751:.1%}')" counted links that are commit links 200/200 98.1% to 100.0% no-link records hiding a link 0/100 0.0% to 3.7% look-alikes that are commit links 1/200 0.09% to 2.78% counting the undecided one as a miss 2/200 0.27% to 3.57% headline at the top of the look-alike interval 12.9% headline at the top of the record interval 15.5%
Intervals are Wilson score intervals at 95 per cent. The look-alike pool is only the URLs that contain commit, changeset, /rev/ or a long hex string, so the direct count above is the better test; this pane shows how far a sample of this size could be trusted without it. The 100 records are the only part of the sample that covers every kind of URL, and on their own they cannot rule out a headline as high as 15.5 per cent. The direct count finds 270 extra records, not the thousands that would take, though it only finds the shapes it was written to look for.
.prior
What others have measured.
This is not the first count of its kind, and the earlier work asks a different question. Hommersom, Sabetta and colleagues, building a tool that ranks candidate fix commits for an advisory, took 2,391 known fix commits for 1,248 vulnerabilities and checked how many the NVD referenced: 7.53 per cent of the commits, and, used as a ranking signal, NVD references found a fix commit for 12.04 per cent of the vulnerabilities on average. Datasets such as CVEfixes and MoreFixes go further and mine the fix commits themselves, from references and from repository history, to build training and evaluation corpora.
We asked something narrower and simpler: how often does the official record, as written by the CNA, name a commit at all? The answer by year, by CNA and by host, up to 2026, and the kernel and GitHub effect that now dominates it, is what this page adds.
- Hommersom, Sabetta et al., automated mapping of advisories onto fix commitsarxiv.org/abs/2103.13375
- Bhandari, Naseer and Moonen, CVEfixes, PROMISE 2021arxiv.org/abs/2107.08760
- Akhoundali et al., MoreFixes, PROMISE 2024doi.org/10.1145/3663533.3664036
- CVE list bulk export used heregithub.com/CVEProject/cvelistV5/releases/tag/cve_2026-09-27_1600Z
- KEV catalogue, pinned commitgithub.com/cisagov/kev-data/commit/9dfdff658ee0fbbca1c648235a29a5402f6533ca
.limits
What this does not show.
- a link is not the fix The count says a record names a commit. It does not say the commit is the fix. Some links point at a backport, a test, a follow-up hardening change, or occasionally the commit that introduced the bug.
- no link is not no fix A record with no commit link may describe a bug that was fixed years ago. The fix can often be found through the project's release notes, a distribution's security tracker, or a search of the repository history for the CVE ID. It just is not in the record.
- records, not maintainers This measures how CVE records are written. It says nothing about how fast or how well anyone fixes vulnerabilities.
- one snapshot The list changes every day. Records gain references after publication, and new CNAs appear. These figures are true of the export pinned above and will drift from it.
- git only Subversion and Mercurial revisions are real revisions. They are counted separately rather than folded in, so the headline is a figure for git.
.eof
Why a binary analysis company counted links.
When you compare two builds of a program to find what a security update changed, as we did in our SQLite piece, the commit that changed the source is the answer key, if you can find it. One record in eight links a commit, and that commit still has to be checked. For the rest the change has to be found some other way, and for a closed-source product in the exploited list there is no public commit at all: the binary is all there is to read.
Violet RE played no part in finding, reporting or fixing any vulnerability counted here, and the service does not match uploads against CVE records or advisories.
Binaries you can't read are binaries you can't trust.
Violet RE recovers readable source from compiled code and checks it for known weakness types, with the limits of each check stated. violetre.io