ANT-2026-RPE1YJCH · universal-ctags/ctags

use-after-free high

Severity Claude high · Security research firm high · Maintainer -

Discovered by an unreleased Anthropic model

REPORT

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

  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

[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

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Ada Logics.

Verdict
true positive
Severity
high
TIMELINE

Dates from discovery through public reveal.

  1. 2026-04-08 Reported to tracker
  2. 2026-04-13 Sent to maintainer
  3. 2026-04-14 Patch released
  4. 2026-05-07 Maintainer acknowledged
  5. 2026-07-08 Publicly revealed
PROVENANCE

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"
}