Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

heap buffer overflow on convert_utf16_to_utf32() #469

Closed
GPTFuzzx opened this issue Jan 22, 2025 · 9 comments
Closed

heap buffer overflow on convert_utf16_to_utf32() #469

GPTFuzzx opened this issue Jan 22, 2025 · 9 comments

Comments

@GPTFuzzx
Copy link

GPTFuzzx commented Jan 22, 2025

Hi .

The libfuzzer harness code :

#include <cstdlib>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <cassert>
#include <climits>
#include <emmintrin.h>
#include <immintrin.h>
#include <algorithm>
#include <cstddef>
#include <random>
#include <ctime>
#include <memory>
#include <sys/time.h>
#include <sys/stat.h>
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sstream>
#include <cstdbool>
#include <csetjmp>
#include "plugins/threaded/include/lcms2_threaded.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    if (size < 1) return 0;
    uint8_t choice = data[0];
    data++;
    size--;
    switch (choice % 4) {
        case 0: {
            if (size >= 1) {
                cmsHPROFILE hProfile = cmsOpenProfileFromMem(data, (cmsUInt32Number)size);
                if (hProfile) {
                    cmsUInt32Number tagCount = cmsGetTagCount(hProfile);
                    for (cmsUInt32Number i = 0; i < tagCount; i++) {
                        cmsTagSignature tagSig = cmsGetTagSignature(hProfile, i);
                        void *tagData = cmsReadTag(hProfile, tagSig);
                        (void)tagData;
                    }
                    cmsCloseProfile(hProfile);
                }
            }
            break;
        }
        case 1: {
            if (size >= 1) {
                cmsHANDLE hIT8 = cmsIT8LoadFromMem(NULL, (const char*)data, (cmsUInt32Number)size);
                if (hIT8) {
                    char **dataFormats;
                    int numDataFormats = cmsIT8EnumDataFormat(hIT8, &dataFormats);
                    if (numDataFormats > 0 && dataFormats) {
                        for (int i = 0; i < numDataFormats; ++i) {
                            (void)dataFormats[i];
                        }
                    }
                    char **properties;
                    cmsUInt32Number numProperties = cmsIT8EnumProperties(hIT8, &properties);
                    if (numProperties > 0 && properties) {
                        for (cmsUInt32Number i = 0; i < numProperties; ++i) {
                            (void)properties[i];
                        }
                    }
                    cmsUInt32Number bytesNeeded = 0;
                    cmsIT8SaveToMem(hIT8, NULL, &bytesNeeded);
                    if (bytesNeeded > 0) {
                        void *buffer = malloc(bytesNeeded);
                        if (buffer) {
                            cmsIT8SaveToMem(hIT8, buffer, &bytesNeeded);
                            free(buffer);
                        }
                    }
                    cmsIT8Free(hIT8);
                }
            }
            break;
        }
        case 2: {
            if (size >= sizeof(cmsFloat64Number)) {
                cmsFloat64Number num;
                memcpy(&num, data, sizeof(cmsFloat64Number));
                cmsUInt16Number result;
                if (num <= 0) result = 0;
                else if (num >= 65535.0) result = 65535;
                else result = (cmsUInt16Number)floor(num + 0.5);
                (void)result;
            }
            break;
        }
        case 3: {
            if (size >= 3) {
                cmsHPROFILE hProfile2 = cmsOpenProfileFromMem(data, (cmsUInt32Number)size);
                if (hProfile2) {
                    cmsHPROFILE hSRGB = cmsCreate_sRGBProfile();
                    if (hSRGB) {
                        cmsHTRANSFORM transform = cmsCreateTransform(hProfile2, TYPE_RGB_8, hSRGB, TYPE_RGB_8, INTENT_PERCEPTUAL, 0);
                        if (transform) {
                            uint8_t input[3] = { data[0], data[1], data[2] };
                            uint8_t output[3];
                            cmsDoTransform(transform, input, output, 1);
                            cmsDeleteTransform(transform);
                        }
                        cmsCloseProfile(hSRGB);
                    }
                    cmsCloseProfile(hProfile2);
                }
            }
            break;
        }
    }
    return 0;
}

Caused the ASAN to complain :

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 289327605
INFO: Loaded 1 modules   (9347 inline 8-bit counters): 9347 [0x563a59a02350, 0x563a59a047d3), 
INFO: Loaded 1 PC tables (9347 PCs): 9347 [0x563a59a047d8,0x563a59a29008), 
/out/API_52_refined_reviewed: Running 1 inputs 1 time(s) each.
Running: /out/traces/quest_28384/crashes/crashcrash-c8653b200c139ace4bc0faec844f71766c80e0b7
=================================================================
==10006==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5020000000b4 at pc 0x563a59911252 bp 0x7ffdf5c68490 sp 0x7ffdf5c68488
WRITE of size 4 at 0x5020000000b4 thread T0
SCARINESS: 36 (4-byte-write-heap-buffer-overflow)
    #0 0x563a59911251 in convert_utf16_to_utf32 /src/lcms/src/cmstypes.c:158:23
    #1 0x563a59911251 in _cmsReadWCharArray /src/lcms/src/cmstypes.c:190:16
    #2 0x563a598fe454 in Type_Text_Description_Read /src/lcms/src/cmstypes.c:1150:10
    #3 0x563a59912306 in ReadEmbeddedText /src/lcms/src/cmstypes.c:3518:30
    #4 0x563a59918a75 in ReadSeqID /src/lcms/src/cmstypes.c:3674:10
    #5 0x563a59914bd5 in ReadPositionTable /src/lcms/src/cmstypes.c:257:14
    #6 0x563a5990b944 in Type_ProfileSequenceId_Read /src/lcms/src/cmstypes.c:3702:10
    #7 0x563a598c7a78 in cmsReadTag /src/lcms/src/cmsio0.c:1728:25
    #8 0x563a598be9e4 in LLVMFuzzerTestOneInput /src/lcms/fuzzers/API_52_refined_reviewed.cc:37:41
    #9 0x563a597733c0 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
    #10 0x563a5975e635 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
    #11 0x563a597640cf in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
    #12 0x563a5978f372 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    #13 0x7efd3b286082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 0702430aef5fa3dda43986563e9ffcc47efbd75e)
    #14 0x563a5975681d in _start (/out/API_52_refined_reviewed+0x8281d)

DEDUP_TOKEN: convert_utf16_to_utf32--_cmsReadWCharArray--Type_Text_Description_Read
0x5020000000b4 is located 0 bytes after 4-byte region [0x5020000000b0,0x5020000000b4)
allocated by thread T0 here:
    #0 0x563a5987f18f in malloc /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:68:3
    #1 0x563a59947c15 in _cmsMalloc /src/lcms/src/cmserr.c:268:12
    #2 0x563a59947c15 in _cmsMallocZeroDefaultFn /src/lcms/src/cmserr.c:107:16
    #3 0x563a598fe42f in Type_Text_Description_Read /src/lcms/src/cmstypes.c:1147:31
    #4 0x563a59912306 in ReadEmbeddedText /src/lcms/src/cmstypes.c:3518:30
    #5 0x563a59918a75 in ReadSeqID /src/lcms/src/cmstypes.c:3674:10
    #6 0x563a59914bd5 in ReadPositionTable /src/lcms/src/cmstypes.c:257:14
    #7 0x563a5990b944 in Type_ProfileSequenceId_Read /src/lcms/src/cmstypes.c:3702:10
    #8 0x563a598c7a78 in cmsReadTag /src/lcms/src/cmsio0.c:1728:25
    #9 0x563a598be9e4 in LLVMFuzzerTestOneInput /src/lcms/fuzzers/API_52_refined_reviewed.cc:37:41
    #10 0x563a597733c0 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
    #11 0x563a5975e635 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
    #12 0x563a597640cf in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
    #13 0x563a5978f372 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    #14 0x7efd3b286082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 0702430aef5fa3dda43986563e9ffcc47efbd75e)

DEDUP_TOKEN: __interceptor_malloc--_cmsMalloc--_cmsMallocZeroDefaultFn
SUMMARY: AddressSanitizer: heap-buffer-overflow /src/lcms/src/cmstypes.c:158:23 in convert_utf16_to_utf32
Shadow bytes around the buggy address:
  0x501ffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x501ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x501fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x501fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x502000000000: fa fa 00 00 fa fa 00 fa fa fa 00 fa fa fa 00 fa
=>0x502000000080: fa fa 00 07 fa fa[04]fa fa fa fa fa fa fa fa fa
  0x502000000100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x502000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x502000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x502000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x502000000300: 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
==10006==ABORTING

This is likely caused by improper bounds checking during UTF string conversion.

lcms commit : c75a82336eeaa19c2632193c63d52c9435542182

The root cause :

Root Cause:

  • The library allocates a buffer for (len16 + 1) UTF-32 code points (where len16 is the number of UTF-16 code units read from the input ICC profile).

  • When len16 = 0 (e.g., an empty string), the allocation becomes 1 * sizeof(cmsUInt32Number) = 4 bytes (enough for 1 UTF-32 code point + null terminator).

  • The code then writes a null terminator to dst[len16 + 1] (i.e., dst[1]), which is out-of-bounds for the 4-byte buffer. This is a classic "off-by-one" error in the library’s string termination logic.

We didn't check the latest lcms against this bug, but if present, should be fixed .

Thanks.

@mm2
Copy link
Owner

mm2 commented Jan 24, 2025

Thanks for reporting. If you could add the data (e.g., the ICC profile) used by the fuzzer to obtain the crash, that would be very useful. I can't spot any defect in the code by just inspecting it, the len16=0 case is covered by a previous "if" and should not reach this function.

@GPTFuzzx
Copy link
Author

Hi .

Attached zip file contains the crashing testcase.

crashcrash-c8653b200c139ace4bc0faec844f71766c80e0b7.zip

@mm2
Copy link
Owner

mm2 commented Jan 24, 2025

Thank you so much. Unfortunately, running your code and this sample results in a "corrupted tag" error with no flaw at all. The "convert_utf16_to_utf32()" routine you report as buggy is never executed, so, I wonder if you just mismatched the sample or perhaps you are using outdated sources?

@GPTFuzzx
Copy link
Author

Did you checked out to the lcms commit as mentioned in the first post ?

c75a82336eeaa19c2632193c63d52c9435542182

Are you able to reproduce the bug with the ASAN ?

@mm2
Copy link
Owner

mm2 commented Jan 24, 2025

The commit c75a823 was on Oct-27, 2024, going back to the 2.16 official release, which is dated Dec 2023 and running this program, the reported routine is never reached, and the profile is discarded because malformed. You can see I call your fuzzer code. On actual code happens exactly same.

int main(int argc, char* argv[])
{
        FILE* file = fopen("crashcrash-c8653b200c139ace4bc0faec844f71766c80e0b7", "rb");
        if (!file) {
            perror("Failed to open file");
            return 1;
        }

        // Seek to the end of the file to determine its size
        fseek(file, 0, SEEK_END);
        long fileSize = ftell(file);
        fseek(file, 0, SEEK_SET);

        // Allocate memory for the file content
        char* buffer = (char*)malloc(fileSize );
        if (!buffer) {
            perror("Failed to allocate memory");
            fclose(file);
            return 1;
        }

        // Read the file into the buffer
        fread(buffer, 1, fileSize, file);
        fclose(file);

        LLVMFuzzerTestOneInput(buffer, fileSize);
        free(buffer);

        return 0;
}

@GPTFuzzx
Copy link
Author

@mm2 Can you please send the output of the ASAN here ?

Thanks.

@mm2
Copy link
Owner

mm2 commented Jan 26, 2025

Finally I have been able to reproduce it. It is very elusive as only happens on linux and only if sizeof(wchar_t) > sizeof(uint16_t). A commit is following. This will automatically close the issue.
Thank you so much for your patience!
Best regards

mm2 added a commit that referenced this issue Jan 26, 2025
Many thanks to @GPTFuzzx for pointing out the issue.
@mm2
Copy link
Owner

mm2 commented Jan 26, 2025

Would you please assign a CVE id to this issue?

It is not exploitable. This tag is used only in reference function "_cmsReadProfileSequence()" which is not used nor accessible from API. Instead, the library computes the sequence by its own. You can do a search in sources if wish so.

*According CVE vulnerability implies exploits https://www.cve.org/ResourcesSupport/Glossary?activeTerm=glossaryVulnerability

@GPTFuzzx
Copy link
Author

@mm2 Apologize, I wanted to ask for CVE for another bug in another project that I reported and it's exploitable, so you can ignore my previous comment and thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants