ANT-2026-RPE1YJCH · universal-ctags/ctags
use-after-free high
Severity Claude high · Security research firm high · Maintainer -
Discovered by an unreleased Anthropic model
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.
ANT-2026-RPE1YJCH: Heap overflow via integer truncation in field array growth
In libreadtags' growFields() (readtags.c:412), the new capacity 2 * file->fields.max is assigned to an unsigned short, so when the array reaches 40960 entries the doubled value 81920 truncates to 16384 and realloc() shrinks the live buffer. Back in parseExtensionFields() the growth guard uses == rather than >=, so with count=40960 and max=16384 it never fires again, and the loop keeps writing field entries at indices 40960..65535. Each write stores two attacker-supplied pointers (key/value into the attacker-controlled line buffer), yielding ~24,576 out-of-bounds 16-byte writes spanning ~400 KB of heap past the shrunken allocation. Any consumer of libreadtags (readtags CLI, editor tag-jump features) that calls tagsFirst/tagsNext/tagsFind on a hostile tags file is affected. A secondary double-free is also latent when max reaches 32768.
Target
Project: ctags
Version: commit 34659e2cb02e055c74e929c5bad291470a729570 (still present at HEAD)
Location: libreadtags/readtags.c:412
Technical Details
Built readtags with ASAN. Crafted a tags file with one tag line carrying 41,000 tab-separated a:b extension fields (~160KB). readtags -t tags -l crashes: ASAN reports heap-use-after-free WRITE of size 8 at parseExtensionFields (libreadtags/readtags.c:509), with the free site at growFields (readtags.c:414) via realloc. The trace shows the 655,360-byte (40960*16) region was freed by realloc when newCount truncated 81920->16384, and the subsequent write at index 40960 lands in freed memory exactly as described. The == growth check never fires again because count(40960) != max(16384). This is a library-level bug reachable by any consumer of libreadtags via tagsFirst/tagsNext/tagsFind on a hostile tags file.
Crash trace (truncated — full trace in attached crash.log):
=================================================================
==7==ERROR: AddressSanitizer: heap-use-after-free on address 0xffffa37bd800 at pc 0xaaaae1b6a5a8 bp 0xffffd4511000 sp 0xffffd4511018
WRITE of size 8 at 0xffffa37bd800 thread T0
#0 0xaaaae1b6a5a4 in parseExtensionFields libreadtags/readtags.c:509
#1 0xaaaae1b6a5a4 in parseTagLine libreadtags/readtags.c:669
#2 0xaaaae1b6aadc in readNext libreadtags/readtags.c:990
#3 0xaaaae1b6c5ac in tagsFirst libreadtags/readtags.c:1333
#4 0xaaaae1b71c20 in listTags extra-cmds/readtags-cmd.c:504
#5 0xaaaae1b74ed0 in run extra-cmds/readtags-cmd.c:670
#6 0xaaaae1b74ed0 in main extra-cmds/readtags-cmd.c:1044
#7 0xffffa41a2258 (/lib/aarch64-linux-gnu/libc.so.6+0x22258) (BuildId: 45918bc10b33fd96afc550c98de062dccdf44328)
[... 26 more frames — full trace in crash.log]
Reproduction
- Craft tags file:
!_TAG_FILE_FORMAT\t2\t//\n!_TAG_FILE_SORTED\t1\t//\nx\tf\t1;"followed by ~41000 repetitions of\ta:band a newline - Distribute it (e.g., commit to a Git repo the victim will clone)
- Victim runs
readtags -t tags -lor opens the repo in an editor that invokes libreadtags - Field array doubles 20→…→40960, then 2*40960=81920 truncates to 16384 and realloc shrinks the buffer
- parseExtensionFields continues writing key/value pointer pairs at indices 40960..65535 past the shrunken allocation
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-RPE1YJCH.
Reference: ANT-2026-RPE1YJCH
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Ada Logics.
- Verdict
- true positive
- Severity
- high
Dates from discovery through public reveal.
- 2026-04-08 Reported to tracker
- 2026-04-13 Sent to maintainer
- 2026-04-14 Patch released
- 2026-05-07 Maintainer acknowledged
- 2026-07-08 Publicly revealed
SHA-3-512 hash:
cff09a38a975cc598ad21e55eb437bfe3c7df0356d8c7d159b7c301ef380f8a87d754153c008c2fc741be870759e567c3262e0256b15782b19ec5bb74b0cd9bf
Committed 2026-04-16 08:59 PT
Revealed 2026-07-08 15:59 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-RPE1YJCH",
"bug_class": "Use-after-free",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-04-09T05:32:11+00:00",
"description": "In libreadtags' growFields() (readtags.c:412), the new capacity `2 * file->fields.max` is assigned to an `unsigned short`, so when the array reaches 40960 entries the doubled value 81920 truncates to 16384 and realloc() shrinks the live buffer. Back in parseExtensionFields() the growth guard uses `==` rather than `>=`, so with count=40960 and max=16384 it never fires again, and the loop keeps writing field entries at indices 40960..65535. Each write stores two attacker-supplied pointers (key/value into the attacker-controlled line buffer), yielding ~24,576 out-of-bounds 16-byte writes spanning ~400 KB of heap past the shrunken allocation. Any consumer of libreadtags (readtags CLI, editor tag-jump features) that calls tagsFirst/tagsNext/tagsFind on a hostile tags file is affected. A secondary double-free is also latent when max reaches 32768.",
"discovered_at": null,
"location": "libreadtags/readtags.c:412",
"poc_sha256": null,
"preimage_version": 1,
"project": "ctags",
"reproduction": [
"1. Craft tags file: `!_TAG_FILE_FORMAT\\t2\\t//\\n!_TAG_FILE_SORTED\\t1\\t//\\nx\\tf\\t1;\"` followed by ~41000 repetitions of `\\ta:b` and a newline",
"2. Distribute it (e.g., commit to a Git repo the victim will clone)",
"3. Victim runs `readtags -t tags -l` or opens the repo in an editor that invokes libreadtags",
"4. Field array doubles 20→…→40960, then 2*40960=81920 truncates to 16384 and realloc shrinks the buffer",
"5. parseExtensionFields continues writing key/value pointer pairs at indices 40960..65535 past the shrunken allocation"
],
"technical_details": "Built readtags with ASAN. Crafted a tags file with one tag line carrying 41,000 tab-separated `a:b` extension fields (~160KB). `readtags -t tags -l` crashes: ASAN reports heap-use-after-free WRITE of size 8 at parseExtensionFields (libreadtags/readtags.c:509), with the free site at growFields (readtags.c:414) via realloc. The trace shows the 655,360-byte (40960*16) region was freed by realloc when newCount truncated 81920->16384, and the subsequent write at index 40960 lands in freed memory exactly as described. The `==` growth check never fires again because count(40960) != max(16384). This is a library-level bug reachable by any consumer of libreadtags via tagsFirst/tagsNext/tagsFind on a hostile tags file.",
"title": "Heap overflow via integer truncation in field array growth",
"vendor_severity": "high"
}