Skip to content

Heap out-of-bounds read in RLE_ANALISE (rle.cpp) #663

@fa1c4

Description

@fa1c4

Summary

There is a heap-based out-of-bounds read in the RLE encoder function RLE_ANALISE in src/rle.cpp. The bug is triggered by crafted input where the buffer length is small (for example, 1 byte). Under AddressSanitizer this manifests as a reproducible heap-buffer-overflow in RLE_ANALISE.


Affected version / commit

  • Repository: KranX/Vangers
  • Commit: 72145feed605856c6711bbbcb4f9db99db3434fd

Environment

  • OS: Ubuntu 22.04.5 LTS
  • Kernel: 5.15.0-160-generic
  • Compiler: clang++ with AddressSanitizer enabled

Actual behavior

Running the RLE encoder on attacker-controlled input can cause a heap-buffer-overflow read in RLE_ANALISE. AddressSanitizer reports:

=================================================================
==14==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c03af6e00b2 at pc 0x55cda1781938 bp 0x7ffc54b79cd0 sp 0x7ffc54b79cc8
READ of size 1 at 0x7c03af6e00b2 thread T0
SCARINESS: 12 (1-byte-read-heap-buffer-overflow)
    #0 0x55cda1781937 in RLE_ANALISE(unsigned char*, int, unsigned char*&) /src/vangers_rle_fuzzer.cc:23:37
    #1 0x55cda1781b7d in LLVMFuzzerTestOneInput /src/vangers_rle_fuzzer.cc:123:27
    #2 0x55cda161eded in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:619:13
    #3 0x55cda1609b62 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:329:6
    #4 0x55cda160fa30 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:865:9
    #5 0x55cda163b562 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    #6 0x7fe3b01a1082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
    #7 0x55cda1602c4d in _start (/out/vangers_rle_fuzzer+0x3cc4d)

0x7c03af6e00b2 is located 0 bytes after 2-byte region [0x7c03af6e00b0,0x7c03af6e00b2)
allocated by thread T0 here:
    #0 0x55cda173d744 in malloc /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:67:3
    #1 0x55cda1782083 in operator new(unsigned long) cxa_noexception.cpp
    #2 0x55cda1609b62 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:329:6
    #3 0x55cda160fa30 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:865:9
    #4 0x55cda163b562 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    #5 0x7fe3b01a1082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)

SUMMARY: AddressSanitizer: heap-buffer-overflow in RLE_ANALISE(unsigned char*, int, unsigned char*&)
=================================================================

Minimal reproducer

Below is a standalone reproducer based on the current src/rle.cpp. It calls RLE_ANALISE with a 1-byte input buffer, compiled with ASan:

#include <cstdint>
#include <cstring>

using uchar = unsigned char;

// Copy of Vangers/src/rle.cpp::RLE_ANALISE (unchanged)
int RLE_ANALISE(uchar* _buf, int len, uchar*& out) {
    int i = 0;
    int pack_len = 0;
    uchar c_len = 0;
    uchar* buf = _buf;
    uchar* _out = new uchar[len*2];
    uchar* p = _out;
    uchar _ch = *buf++;

    while (i < len) {
        while ((i < len) && (_ch == *buf) && (c_len < 127)) {
            c_len++;
            buf++;
            i++;
        }

        if (c_len) {
            *p++ = c_len;
            *p++ = _ch;

            _ch = *buf++;

            pack_len += 2;
            c_len = 0;
            i++;
        }

        while ((i < len) && (_ch != *buf) && (c_len < 127)) {
            c_len++;
            _ch = *buf++;
            i++;
        }

        if (c_len) {
            *p++ = 128 + (c_len - 1);
            std::memcpy(p, buf - c_len - 1, c_len);
            p += c_len;
            pack_len += c_len + 1;
            c_len = 0;
        }
    }

    out = new uchar[pack_len];
    std::memcpy(out, _out, pack_len);
    delete[] _out;

    return pack_len;
}

int main() {
    uchar in[1] = {0x40};  // any value is sufficient
    uchar* out = nullptr;
    RLE_ANALISE(in, 1, out);
    delete[] out;
    return 0;
}

Build & run (for example):

clang++ -O1 -g -fsanitize=address -fno-omit-frame-pointer poc.cpp -o poc
./poc

On my environment this reliably reports a heap-buffer-overflow in RLE_ANALISE.


Root cause analysis

The core issue is that RLE_ANALISE consumes one byte from _buf and advances buf before the main loop, but does not update the loop counter i accordingly:

int i = 0;
uchar* buf = _buf;
uchar _ch = *buf++;   // consumes _buf[0], buf now points to _buf + 1

At this point:

  • buf == _buf + 1
  • i == 0
  • len >= 1 (for the reproducer, len == 1)

Then the outer loop starts:

while (i < len) {
    while ((i < len) && (_ch == *buf) && (c_len < 127)) {
        c_len++;
        buf++;
        i++;
    }
    ...
}

For len == 1, the condition i < len is true (0 < 1), so the inner while evaluates (_ch == *buf). But buf already points one past the end of the input buffer (_buf + 1), so *buf dereferences memory just beyond the valid region. This causes a 1-byte heap out-of-bounds read on the input buffer.

In other words, the logical counter i and the pointer buf are out of sync after the initial *buf++:

  • The function has already consumed 1 byte from _buf,
  • but i still indicates “0 bytes processed”, so the i < len check does not prevent the out-of-bounds access.

Additionally, the function unconditionally does uchar _ch = *buf++; even when len might be 0, so calling it with len == 0 and a null/invalid _buf would also be undefined behavior. There is no defensive check on len at function entry.


Suggested fix

One straightforward fix is to keep i consistent with the number of bytes consumed from _buf. For example, after the initial read:

int i = 0;
uchar* buf = _buf;
if (len <= 0) {
    out = nullptr;
    return 0;
}

uchar _ch = *buf++;
i = 1;              // keep i in sync with buf

Alternatively, the loops can be rewritten to use pointer comparisons against _buf + len (e.g. buf < _buf + len) instead of relying on i.


Impact

From a security perspective this is a heap-based out-of-bounds read that can cause a crash (denial of service) when processing untrusted input that reaches RLE_ANALISE. In practice, the observed impact is a process crash under ASan. Since the read is only one byte beyond the input buffer, information disclosure potential is limited, but the behavior is still undefined in normal builds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions