Trail of Bits’ Codex Backporting Turns Stable-Kernel Security Into a Review-Hours Problem, but 143 of Its 206 Deliveries Look Like Plain Cherry-Picks and the Bug Behind the OpenAI Hack Had No CVE
Trail of Bits says Codex-assisted work delivered fixes for 149 CVEs into stable Linux kernels; the project’s tracker reproduces the count, but most deliveries look like plain cherry-picks, one stable...
On November 2, Trail of Bits published “Backporting security fixes at scale with Codex”, a post by Artem Dinaburg about getting security fixes into the stable Linux kernel trees and into distribution packages. The firm does the work under Patch the Planet, which its own page calls “our joint initiative with OpenAI to assist critical open-source software in the age of machine-speed vulnerability research.” The headline figure is “By October 29, our project helped deliver fixes for 149 distinct CVEs into stable Linux kernel releases, and several fixes to Fedora and Ubuntu packages.”
Table Of Content
- What we found
- What the post claims
- The 149 reproduces, and the tracker shows how it is built
- How the 149 is spread
- What the commit messages show
- The backlog table reproduces, but it is a moving target
- One release moved more than the project did
- The branches with the biggest backlog are outside the tracker
- The severity columns describe a minority of the CVEs
- What the per-fix times imply at this scale
- The distro table depends on which Debian flags you honor
- The libheif case: a missing backport that had no CVE
- Where CVE-2026-98432 stood on November 2, “weeks after being patched upstream”
- What would settle the open questions
- Method and sources
We checked that claim against the data behind it: the project’s public tracker, the kernel CVE team’s vulns.git at three dates, the ChangeLogs of the stable releases that carry the fixes, and the Debian security tracker. The count holds. Several things around it need a caveat.
What we found
- The 149 reproduces exactly from the tracker’s data: 149 distinct CVEs, 206 CVE-by-tree deliveries in the 6.1, 6.6, 6.12 and 6.18 trees, and every one of the 206 stable commits appears in a kernel.org ChangeLog.
- 143 of the 206 look like plain cherry-picks, with no Trail of Bits sign-off and no backport note. The other 63 carry a sign-off from one engineer, and 48 of those include a “Backport to” note.
- The post’s backlog table reproduces to within 10 percent from
vulns.git, but it is a snapshot. The plain count for 6.1.y fell from 1,244 on September 18 to 550 on November 2, mostly because one release, 6.1.191, carried 1,316 CVE fixes, 16 of them with a Trail of Bits sign-off. - 5.10.y and 5.15.y, which top the post’s table, are not in the tracker and are projected to reach end of life in December 2026.
- The case the post cites for the stakes, an old vulnerability used in the OpenAI hack, was a libheif bug that, in the finders’ words, “received no CVE.” A table that counts CVE records cannot see that kind of bug.
That leaves the post’s real proposition: the cost of one fix. A straightforward backport “takes about 30 minutes of human attention” and a more involved one “around three hours.” The public data cannot confirm those times, but it does show how many fixes there are to review.
What the post claims
- Delivery. 149 distinct CVEs in stable kernels by October 29, plus “several fixes to Fedora and Ubuntu packages.”
- Cost per fix. “Excluding project-specific initial setup, preparing and reviewing a straightforward backport takes about 30 minutes of human attention; a more involved case can take around three hours.” The post adds: “Those estimates only cover patch preparation and review; regression testing and maintainer acceptance remain essential.”
- Backlog. Two tables. The first lists CVEs “per supported stable branch that do not have a fix in that branch, but do have it in a newer branch,” from the kernel CVE team’s
vulns.git“as of September 18, 2026,” with the warning “This is not an exact bug count, as it both over- and under-counts due to several technicalities.” The second lists non-kernel CVEs “reported as open or affected” in Debian 13 and Ubuntu 26.04, which the post calls “distro-metadata findings, not individually source-confirmed vulnerabilities.” - Stakes. The post says “an old vulnerability in stable software was used as a part of the recently disclosed OpenAI hack,” and that CVE-2026-98432 “still had not landed in distribution kernels weeks after being patched upstream.”
The 149 reproduces, and the tracker shows how it is built
The post links a read-only tracker. Its app.js loads a data.json file. The copy we read was generated on October 30 at 03:31 UTC from vulns.git commit 604accad (October 28) and stable-queue commit 7f27b6ac (October 29). It holds 975 CVE rows, each assessed in five trees: 6.1.y at v6.1.190, 6.6.y at v6.6.160, 6.12.y at v6.12.114, 6.18.y at v6.18.59 and 7.2.y at v7.2.13. The page says its rows “cover project deliveries, the complete applicable unfixed backlog, uncertain applicability, and recorded semantic reviews.” A short script reproduces the headline count.
# repro_149.py
# Recount the Trail of Bits tracker's project-credited CVEs.
# Usage: python repro_149.py (reads the live data.json)
# python repro_149.py data.json (reads a saved copy)
import json
import sys
import urllib.request
URL = "https://trailofbits.github.io/ptp-linux-stable-cve-dashboard/data.json"
if len(sys.argv) > 1:
data = json.load(open(sys.argv[1], encoding="utf-8"))
else:
request = urllib.request.Request(URL, headers={"User-Agent": "Mozilla/5.0"})
data = json.load(urllib.request.urlopen(request))
print("generated_at:", data["generated_at"])
delivered = {} # CVE id -> trees where a project-credited fix is already in a stable release
queued = set() # CVE ids with a project-credited fix waiting in the stable queue
for tree in data["trees"]:
per_tree = 0
for record in tree["records"]:
if not record["project"]:
continue
if record["status"] == "backported":
delivered.setdefault(record["id"], []).append(tree["series"])
per_tree += 1
elif record["status"] == "queued":
queued.add(record["id"])
print(f" {tree['series']:<7} (checked at v{tree['version']}): {per_tree} project deliveries")
print("distinct CVEs delivered:", len(delivered))
print("CVE-by-tree deliveries: ", sum(len(trees) for trees in delivered.values()))
print("CVEs with a project fix queued:", len(queued))
On the October 30 snapshot (the live file changes as the project adds work), it prints:
generated_at: 2026-10-30T03:31:28.522226+00:00
6.1.y (checked at v6.1.190): 63 project deliveries
6.6.y (checked at v6.6.160): 81 project deliveries
6.12.y (checked at v6.12.114): 30 project deliveries
6.18.y (checked at v6.18.59): 32 project deliveries
7.2.y (checked at v7.2.13): 0 project deliveries
distinct CVEs delivered: 149
CVE-by-tree deliveries: 206
CVEs with a project fix queued: 16
The count is distinct CVEs, as the post says, and the tracker’s own page notes “A CVE fixed in two trees counts once.” Table 1 shows every tree’s rows by status. Each row sums to 975.
| Tree (checked at) | Project fix released | Project fix queued | Others’ fix released | Others’ fix queued | Applicable, unfixed | Not applicable | Unknown |
|---|---|---|---|---|---|---|---|
| 6.1.y (6.1.190) | 63 | 15 | 11 | 108 | 516 | 256 | 6 |
| 6.6.y (6.6.160) | 81 | 1 | 389 | 2 | 331 | 166 | 5 |
| 6.12.y (6.12.114) | 30 | 0 | 666 | 3 | 231 | 41 | 4 |
| 6.18.y (6.18.59) | 32 | 0 | 804 | 1 | 127 | 7 | 4 |
| 7.2.y (7.2.13) | 0 | 0 | 967 | 0 | 2 | 6 | 0 |
How the 149 is spread
Of the 149 CVEs, 119 were delivered to a single tree (57 to 6.6.y only, 45 to 6.1.y only, 10 to 6.18.y only and 7 to 6.12.y only), 14 to two trees, 5 to three and 11 to all four. Sixteen CVEs had a project fix waiting in the stable queue on the snapshot date; 14 of them are also delivered in another tree, and two had no release yet.
We matched each delivery’s stable commit id from the tracker against the ChangeLog files that kernel.org publishes for each stable release. All 206 turned up, in 13 releases over eight weeks.
| Tree | Stable releases (date), commits credited to the project |
|---|---|
| 6.1.y | 6.1.185 (Aug 27): 1; 6.1.187 (Sep 2): 19; 6.1.188 (Sep 14): 15; 6.1.189 (Oct 3): 28 |
| 6.6.y | 6.6.156 (Sep 2): 4; 6.6.157 (Sep 14): 15; 6.6.158 (Oct 3): 25; 6.6.160 (Oct 21): 37 |
| 6.12.y | 6.12.109 (Sep 7): 8; 6.12.110 (Sep 14): 3; 6.12.111 (Sep 21): 19 |
| 6.18.y | 6.18.50 (Sep 7): 9; 6.18.53 (Sep 21): 23 |
What the commit messages show
The messages split the 206 into two groups.
- 143 apparent cherry-picks. These carry no Trail of Bits sign-off, no backport note and nothing in the message that says the patch was adapted. They match the post’s first phase: “the equivalent of finding money on the sidewalk,” fixes “that apply cleanly to the stable kernel trees, but the fix was just never pulled into the kernel,” found with Codex, after which the project “asked the maintainers to apply the existing patches. They did, graciously.” The tracker does not say how it assigns credit, so tying these 143 to that phase is our inference. The post’s own verb is “helped deliver,” which fits that split.
- 63 commits with a sign-off. Every one carries
Signed-off-by: Artem Dinaburg <[email protected]>, the only Trail of Bits sign-off among the 206, ahead of the stable maintainer’s. All 63 have anAssisted-byline: 58 say only “LLM” and five name a tool and model. Forty-eight include a note that begins “Backport to”, such as “Backport to 6.1.y: use timer_shutdown_sync(), already available here, so the timer cannot rearm before the queued work is flushed.” By our keyword reading, 38 of the 48 describe code that differs from upstream and 10 say the change is unchanged apart from its location or context. The kernel’s stable rules ask for exactly this: “If the submitted patch deviates from the original upstream patch (for example because it had to be adjusted for the older API), this must be very clearly documented and justified in the patch description.”
Across all 206, 136 commits have no Assisted-by line, 58 say only “LLM” and 12 name a tool and model: Claude, Codex (“GPT-5”), Antigravity, Jetski and Cursor. None names GPT-5.6, the model the post says found the missing fixes. That is not a discrepancy, since finding a fix needs no trailer, but it means the trailers cannot show how much of the patch text a model wrote. We reported earlier that the kernel is retiring that tag while use of it was still rising.
On quality, we searched the ChangeLogs of 18 recent releases, through the October 30 builds, for a revert or a Fixes: tag that names any of the 206 stable commits. We found none. The window is one to ten weeks, so that is a floor, not a measurement.
The backlog table reproduces, but it is a moving target
The post’s source is the kernel CVE team’s vulns.git. Each published CNA record says where a bug appears and which stable releases fixed it. We took the repository at three commits: fc0b7bc0, the last one before September 19 UTC; 604accad, the tracker’s own snapshot from October 28; and ca1f3afa, from November 2. The script counts CVEs whose record still marks the tip of a branch as affected, which is the post’s rule of CVEs that “do not have a fix in that branch” as we read it. Each snapshot is a 35 to 38 MB tarball, and the pinned links above make the counts repeatable.
# unfixed_by_branch.py
# Count kernel CVEs whose CNA record still marks the tip of a stable branch as affected.
# Usage: python unfixed_by_branch.py vulns-snapshot.tar.gz (Python 3.9+, standard library only)
import json
import sys
import tarfile
INF = 10**9
BRANCHES = ["5.10", "5.15", "6.1", "6.6", "6.12", "6.18"]
def ver(text):
"""'6.1.190' -> (6, 1, 190); '5.10.*' -> (5, 10, INF); '*' -> (INF, INF, INF)"""
if text == "*":
return (INF, INF, INF)
parts = [INF if p == "*" else int(p) for p in text.split(".")]
return tuple((parts + [0, 0])[:3])
def covers(entry, tip):
"""does one CNA version entry cover the given version?"""
low = (0, 0, 0) if entry["version"] == "0" else ver(entry["version"])
if "lessThan" in entry:
return low <= tip < ver(entry["lessThan"])
if "lessThanOrEqual" in entry:
return low <= tip <= ver(entry["lessThanOrEqual"])
return tip == low
def version_block(cna):
"""the CNA block that lists releases (versionType semver/custom), not git commit ids"""
for block in cna.get("affected", []):
entries = []
for entry in block.get("versions", []):
if entry.get("versionType") == "git":
continue
try:
covers(entry, (0, 0, 0))
except ValueError: # a commit id in a version field: skip the entry
continue
entries.append(entry)
if any(e.get("versionType") in ("semver", "custom", "original_commit_for_fix") for e in entries):
return block.get("defaultStatus", "affected"), entries
return None
def unfixed_at_tip(default, entries, tip):
if any(e["status"] == "unaffected" and covers(e, tip) for e in entries):
return False
if default == "affected":
return True
return any(e["status"] == "affected" and covers(e, tip) for e in entries)
counts = dict.fromkeys(BRANCHES, 0)
records = 0
with tarfile.open(sys.argv[1], "r:gz") as tar:
for member in tar:
if not (member.name.startswith("cve/published/") and member.name.endswith(".json")):
continue
found = version_block(json.load(tar.extractfile(member))["containers"]["cna"])
if found is None:
continue
records += 1
for branch in BRANCHES:
major, minor = map(int, branch.split("."))
if unfixed_at_tip(*found, (major, minor, INF - 1)):
counts[branch] += 1
print("published records with a release list:", records)
for branch in BRANCHES:
print(f"{branch}.y {counts[branch]:>5}")
Run on the November 2 snapshot, it prints:
published records with a release list: 18190
5.10.y 2051
5.15.y 1939
6.1.y 550
6.6.y 347
6.12.y 215
6.18.y 119
| Branch | Post (Sep 18) | Our count, Sep 18 | Ours as a share of the post’s | Our count, Oct 28 | Our count, Nov 2 |
|---|---|---|---|---|---|
| 5.10.y | 2,006 | 1,888 | 94% | 2,307 | 2,051 |
| 5.15.y | 1,889 | 1,779 | 94% | 2,231 | 1,939 |
| 6.1.y | 1,329 | 1,244 | 94% | 1,703 | 550 |
| 6.6.y | 1,012 | 927 | 92% | 402 | 347 |
| 6.12.y | 686 | 623 | 91% | 275 | 215 |
| 6.18.y | 337 | 305 | 91% | 122 | 119 |
Our September 18 counts run 6 to 10 percent below the post’s in every branch, which points to a small difference in rules, not a different picture; the post itself says its count “over- and under-counts.” The ranking and the order of magnitude hold.
One release moved more than the project did
The later columns show that the table is a snapshot. For 6.1.y the plain count was 1,244 on September 18, 1,703 on the tracker’s snapshot day and 550 five days later. The drop is one release. 6.1.191, published on October 30, has 1,881 commits, and 1,316 of them are the CNA-listed fix for a published CVE (we matched each commit’s [ Upstream commit <sha> ] id against the .sha1 files in vulns.git). Sixteen carry a Trail of Bits sign-off. That is 1.2 percent. Table 4 puts the project’s releases beside the totals.
| Release (date) | Commits | CVE-fix commits | Project-credited | Share |
|---|---|---|---|---|
| 6.1.188 (Sep 14) | 1,185 | 558 | 15 | 2.7% |
| 6.1.189 (Oct 3) | 968 | 491 | 28 | 5.7% |
| 6.6.160 (Oct 21) | 1,644 | 1,129 | 37 | 3.3% |
| 6.1.191 (Oct 30) | 1,881 | 1,316 | 16 (by sign-off) | 1.2% |
| 5.10.273 (Oct 30) | 517 | 295 | 1 (by sign-off) | 0.3% |
| 5.15.224 (Oct 30) | 572 | 339 | 1 (by sign-off) | 0.3% |
None of this undercuts the 149. It sets the scale: the stable team shipped more CVE fixes in a single release than the project has delivered in total, so a backlog table taken on one date is as much a record of the release calendar as of the backlog.
The tracker’s rows already resembled the post-release picture. On its own snapshot, the plain count for 6.1.y was 1,703. The tracker lists 467 of those as unfixed, 110 as queued, 48 as not applicable and 6 as unknown; the other 1,072 are not rows. In the November 2 data, 1,007 of those 1,072 are fixed in 6.1.y, which is what 6.1.191 delivered. We could not find a rule on the tracker’s page that explains the absence, so we read nothing into it beyond this: its 516 unfixed rows were already close to the plain count after the release (550).
The branches with the biggest backlog are outside the tracker
The post’s table puts 5.10.y at 2,006 and 5.15.y at 1,889 unfixed CVEs, the largest of the six. Neither is a tracker tree, and none of the 206 deliveries went to either; the October 30 releases 5.10.273 and 5.15.224 carry one Trail of Bits sign-off each. The kernel.org releases page lists both as longterm trees with a projected end of life of December 2026, against December 2027 for 6.1 and 6.6 and December 2028 for 6.12 and 6.18. In our counts they rose between September 18 and November 2 (1,888 to 2,051 and 1,779 to 1,939) while the four newer branches fell.
The severity columns describe a minority of the CVEs
The table’s third column is headed “% CVEs with an NVD Score” and the fourth “High or Critical Severity.” The numbers fit one reading in all six rows: the third column is the share of the unfixed CVEs that NVD has scored, and the fourth is a count with its share of that scored subset. For 5.10.y, 213 at 19.0 percent implies about 1,120 scored CVEs, which is 55.8 percent of 2,006. The scored share falls to 15.1 percent for 6.18.y, about 51 CVEs, 3 of them High or Critical. So the High or Critical shares describe a minority, thinnest in the newest branches where NVD has not caught up, and the post itself says the table is “not a measure of exploitability.”
What the per-fix times imply at this scale
The public data cannot confirm “about 30 minutes” or “around three hours,” and the post says they exclude setup, regression testing and maintainer acceptance. They can still size the work. Table 5 multiplies each count by the post’s two figures and converts hours into 40-hour weeks. It is our arithmetic. The real mix of straightforward and involved cases is unknown, although 143 of the 206 deliveries show no sign of adaptation, which points toward the low end for those.
| Work item | Count | At 30 minutes each | At 3 hours each |
|---|---|---|---|
| Project deliveries so far (CVE by tree) | 206 | 103 hours (2.6 weeks) | 618 hours (15.5 weeks) |
| Tracker rows marked applicable and unfixed (516 + 331 + 231 + 127 + 2) | 1,207 | 604 hours (15.1 weeks) | 3,621 hours (90.5 weeks) |
| 5.10.y plus 5.15.y at the post’s counts (2,006 + 1,889), an upper bound | 3,895 | 1,948 hours (48.7 weeks) | 11,685 hours (292.1 weeks) |
The third row is an upper bound because the post says its counts over-count, and they include CVEs that may not apply to those branches. The first row carries a human detail: every one of the 63 sign-offs is the same engineer’s. The post frames the agents as doing the drafting and the engineers as the reviewers, so review capacity is the constraint, and no tracker column measures it.
The distro table depends on which Debian flags you honor
The post’s second table gives 1,128 source packages and 4,562 unique CVEs for Debian 13, and 1,028 packages and 770 CVEs for Ubuntu 26.04, compiled by Codex from the distributions’ bulk feeds. The rule is not published, so we could not reproduce it exactly, and we did not complete a recount of Ubuntu: its CVE API returns 20 records per call and our pagination run did not finish. For Debian we used the security tracker’s JSON file (87.7 MB, fetched November 2), left out the kernel (source package linux) and the tracker’s TEMP- placeholder ids, and counted trixie entries with status open. Table 6 shows how the figure moves with Debian’s own triage fields.
| Rule | Unique CVE ids | Source packages |
|---|---|---|
| Status open | 6,886 | 1,420 |
Minus urgency unimportant |
5,427 | 1,054 |
| Minus entries flagged no-DSA | 2,859 | 706 |
| Minus both | 1,400 | 213 |
The post’s 4,562 sits inside that range. Of the 7,343 open rows with a CVE id, 5,809 have the urgency not yet assigned, which means Debian has not triaged them, 1,522 are unimportant and 12 are low. None is medium or high. Read that way, the Debian figure is an inventory of unclassified entries: it says how much work remains to classify, not how many exploitable flaws are unpatched. That matches the post’s own caution that these are “distro-metadata findings.”
The libheif case: a missing backport that had no CVE
The post’s evidence that gaps in stable software matter is the OpenAI hack, which it links to Hacktron’s write-up of September 13. Hacktron says it chained two bugs on July 25: a libheif heap overflow, reached through Discourse’s image-upload path and ImageMagick, and an OpenAI single-sign-on flaw. “The entire timeline from initial discovery to access to OpenAI repo access took place in less than 72 hours,” it writes, and OpenAI confirmed a fix on its side about 14 hours after the report. The libheif half is the backporting story. An agent session asked to inspect the installed package “found that some particular security fixes were not back-ported to the libheif package,” and Hacktron adds: “Interestingly, the vulnerable code had been changed upstream the previous year, but the commit was not documented as a security fix and received no CVE.” Discourse’s Debian 12 image installed libheif 1.19.7, and “Even Debian 13 still shipped the vulnerable version 1.19.8 at the time. Since then, Debian has published its security update for Debian 13 on August 8, 2026.”
We checked the dates. The upstream commit Hacktron cites, “simplify overlay overlap area computation,” was authored on May 5, 2025, about 15 months before Debian’s update. DSA-6417-1 lists twelve CVE ids and a fixed trixie version of 1.19.8-1+deb13u1. A commit without a CVE cannot appear in that list whether or not the package carries it, and we did not inspect the package contents. We did not test any exploit.
The point for the post’s numbers is that both tables count CVE records. The post says so: “we can limit ourselves to known bugs (with assigned CVEs).” The kernel’s CNA assigns CVEs to a very large number of fixes (18,190 published records in the November 2 snapshot), which is part of why the kernel table is long. libheif’s maintainers did not label this one, and Hacktron suggests that is why Debian was late: “This might be a reason why Debian 12 and 13 have not received the security relevant backports in time.” A backlog counted in CVE ids measures the labeled part of the problem. The unlabeled part is what Hacktron’s agent found by comparing an installed package with upstream, the same comparison the post proposes defenders automate. We covered an earlier libheif flaw in “Wordfence’s AI Testing Framework Found a Critical Flaw in the Library Behind Every iPhone Photo.”
Where CVE-2026-98432 stood on November 2, “weeks after being patched upstream”
Our October 31 post found Red Hat and Ubuntu still without a patched kernel for this TCP bug. Two days later the picture has moved. The fix commit, “ipv4: free inet_opt and ireq_opt after an RCU grace period,” was authored on October 1 and applied on October 5; stable releases carrying it, including 6.1.190 and 5.10.272, came on October 15. Red Hat’s CVE record lists RHSA-2026:93412 (RHEL 9), 93413 (RHEL 10), 93414 and 93415 (RHEL 8 kernel and kernel-rt), all released November 1, 27 days after the upstream commit. Debian’s tracker lists the bug resolved in bookworm-security (6.1.190-1) and trixie-security (6.12.115-2). Ubuntu’s tracker, last updated October 30, still lists the kernel packages for 22.04, 24.04 and 26.04 as pending. The post’s “weeks” holds.
What would settle the open questions
- Per-fix hours. The post gives two numbers and no distribution. Time per tracker row would let reviewers price the 1,207 applicable rows.
- Regression record. We found no reverts or
Fixes:tags in a short window. A longer window, and a count of fixes that needed maintainer rework, would show the review burden more directly. - The two oldest branches. 5.10.y and 5.15.y carry the largest counts, have about two months of projected life left and have no tracker rows.
- Credit rules. The tracker credits 143 commits that carry no Trail of Bits sign-off. A line on how credit is assigned would make the 149 comparable with other contributors’ work.
- The unlabeled class. The bug behind the OpenAI hack had no CVE. Counting that class means diffing packages against upstream, which is what the post’s workflow does for the CVE-labeled slice.
Method and sources
Everything was fetched on November 2, 2026. The post and the Patch the Planet page are Trail of Bits’. The tracker data is the data.json linked above. The vulns.git snapshots come from kernel.googlesource.com by commit id. Release contents come from the ChangeLog files under cdn.kernel.org, for example ChangeLog-6.1.191. A commit counts as a CVE fix when its upstream commit id is the fix id in a published record. “Project-credited” in this article means the tracker’s project flag. The Debian numbers come from the tracker JSON linked above, and the Red Hat, Debian and Ubuntu statuses for CVE-2026-98432 from their public CVE records. One small check on the post’s framing: Ubuntu’s release-team page does say “An estimated 95% of all Ubuntu installations are LTS releases.”
Related coverage: six kernel bugs and the patch pipeline, the Brotli flaw found with OpenAI’s Codex Security and WordPress 7.1.4 and the missing older-branch backports.








No Comment! Be the first one.