Project: libgd (libgd/libgd), language: c++
Vulnerability instance: libgd.cve-2021-40145
Sanitizer: address

================= Bug Report (1/1) ==================
## Source: GitHub Issue
## URL: https://github.com/libgd/libgd/issues/700
## Description:
Issue: libgd/libgd#700
Title: gdImageGdPtr memory leak
State: closed
Created by: meweez
Created at: 2021-05-26 08:24:45+00:00
Labels: feedback requested

Issue Body:
Hello,

I found that 'gdImageGdPtr' in gd_gd.c and 'gdImageWebpPtr' in gd_webp.c are similar functions for different picture formats. You have changed 'gdImageWebpPtr' because of CVE-2016-6912 (double free), So It seems that you need to change 'gdImageGdPtr' too.
I run two test files with ASAN, and the result is shown below. The test files are located in the 'tests/webp' folder.

Test1:
```
#include "gd.h"
#include "gdtest.h"


int main()
{
gdImagePtr im1, im2;
int size;

im1 = NULL;
im2 = gdImageGdPtr(im1, &size);
gdTestAssert(im2 == NULL);

gdImageDestroy(im1);

return gdNumFailures();
}
```
ASAN result:
```
AddressSanitizer:DEADLYSIGNAL
=================================================================
==4303==ERROR: AddressSanitizer: SEGV on unknown address 0x000000001c50 (pc 0x7f368bd976d8 bp 0x7ffd77fc7d70 sp 0x7ffd77fc7d60 T0)
==4303==The signal is caused by a READ memory access.
#0 0x7f368bd976d7 in _gdPutHeader /libgd/src/gd_gd.c:353
#1 0x7f368bd977d6 in _gdImageGd /libgd/src/gd_gd.c:370
#2 0x7f368bd97aec in gdImageGdPtr /libgd/src/gd_gd.c:403
#3 0x557763c46493 in main /libgd/tests/webp/gd_mytest.c:12
#4 0x7f368b68309a in __libc_start_main ../csu/libc-start.c:308
#5 0x557763c46339 in _start (/libgd/build/Bin/test_webp_gd_mytest+0x2339)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /libgd/src/gd_gd.c:353 in _gdPutHeader
==4303==ABORTING
```
Test2:
```
#include "gd.h"
#include "gdtest.h"


int main()
{
gdImagePtr im1, im2;
FILE *fp;
int size;

fp = gdTestFileOpen2("webp", "bug_double_free.jpg");
gdTestAssert(fp != NULL);
im1 = gdImageCreateFromJpeg(fp);
gdTestAssert(im1 != NULL);
fclose(fp);


im2 = gdImageGdPtr(im1, &size);
gdTestAssert(im2 == NULL);

gdImageDestroy(im1);

return gdNumFailures();
}
```
ASAN result:
```
=================================================================
==4395==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 33190811 byte(s) in 1 object(s) allocated from:
#0 0x7f2a27953720 in __interceptor_realloc (/lib/x86_64-linux-gnu/libasan.so.5+0xe9720)
#1 0x7f2a2768fb9d in gdRealloc /libgd/src/gdhelpers.c:81
#2 0x7f2a2766cb8d in gdReallocDynamic /libgd/src/gd_io_dp.c:407
#3 0x7f2a2766ce07 in trimDynamic /libgd/src/gd_io_dp.c:437
#4 0x7f2a2766b6ea in gdDPExtractData /libgd/src/gd_io_dp.c:128
#5 0x7f2a27650aff in gdImageGdPtr /libgd/src/gd_gd.c:404
#6 0x5570c9a34525 in main /libgd/tests/webp/gd_mytest.c:20
#7 0x7f2a26f3c09a in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: 33190811 byte(s) leaked in 1 allocation(s).
```

Is there another CVE here?


Comments:
Comment by cmb69 on 2021-05-26 13:01:06+00:00:
The first test script is bogus, since ìm1` is not initialized. The second test script does not `gdImageDestroy(im2)` which apperars to be the cause of the memory leak.

Anyhow, this is not related to CVE-2016-6912, because that was about a double-free in case `gdImageWebpCtx()` failed, but `_gdImageGd()` cannot fail.

---

Comment by meweez on 2021-05-26 16:10:00+00:00:
Sorry I had a typo in test 1 and `im1 = NULL;` but there is no check for this exception in `_gdImageGd`.

You are using some conditional checks in `_gdImageWebpCtx` and then return 0 or one based on that to prevent double free and other problems. why don't you use such similar conditions in `_gdImageGd` too?


the second test is the test file you are using [here](https://github.com/libgd/libgd/blob/master/tests/webp/bug_double_free.c) with a little change that I replace `gdImageWebpPtr` with `gdImageGdPtr`.
when I added gdImageDestroy(im2) the result is here.

```
/libgd/tests/webp/gd_mytest.c:21: Assert failed in </libgd/tests/webp/gd_mytest.c:21>
AddressSanitizer:DEADLYSIGNAL
=================================================================
==4659==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7fefd9b0a9fb bp 0x7ffcb9b0f7c0 sp 0x7ffcb9b0f7a0 T0)
==4659==The signal is caused by a READ memory access.
==4659==Hint: address points to the zero page.
    #0 0x7fefd9b0a9fa in gdImageDestroy /libgd/src/gd.c:392
    #1 0x563ab073556b in main /libgd/tests/webp/gd_mytest.c:24
    #2 0x7fefd942009a in __libc_start_main ../csu/libc-start.c:308
    #3 0x563ab0735349 in _start (/libgd/build/Bin/test_webp_gd_mytest+0x2349)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /libgd/src/gd.c:392 in gdImageDestroy
==4659==ABORTING
```

---

Comment by vapier on 2021-05-26 17:02:02+00:00:
if you pass garbage to libgd, you can only expect garbage back.  it is not gd's responsibility to make sure programmers don't pass it garbage.  that includes, but is not limited to, programmers passing in NULL pointers.

gd guarantees that failures coming from the OS are handled gracefully when possible, as are corrupt input files/images.

---

Comment by cmb69 on 2021-05-26 19:15:36+00:00:
> when I added gdImageDestroy(im2) the result is here.

If `im2==NULL` this is to be expected; hoewever, I cannot reproduce this; for me the test fails on that assertion.

---

Comment by meweez on 2021-05-26 20:35:50+00:00:
I'm using this:
```
#include "gd.h"
#include "gdtest.h"


int main()
{
    gdImagePtr im1, im2;
    FILE *fp;
    int size;

    fp = gdTestFileOpen2("webp", "bug_double_free.jpg");
    gdTestAssert(fp != NULL);
    im1 = gdImageCreateFromJpeg(fp);
    gdTestAssert(im1 != NULL);
    fclose(fp);

  
    im2 = gdImageGdPtr(im1, &size);
    gdTestAssert(im2 == NULL);

    gdImageDestroy(im1);
    if(im2 != NULL)
        gdImageDestroy(im2);

    return gdNumFailures();
}
```


---

Comment by meweez on 2021-05-26 20:39:02+00:00:
> gd guarantees that failures coming from the OS are handled gracefully when possible, as are corrupt input files/images.

Sorry,  but I couldn't understand why you are using multiple checks on `_gdImageWebpCtx` if it is not gd's responsibility?

thanks

---

Comment by vapier on 2021-05-26 20:56:14+00:00:
"some places check for NULL" is not the same thing as "gd guarantees you can pass it NULL pointers".  if you have code passing NULL pointers to functions, your code is bad and needs fixing.  it is not a bug in gd.

we should be able to add non-null attributes to our functions so the compiler could try and enforce that too.

---

Comment by cmb69 on 2021-05-26 21:18:13+00:00:
It seems to me a very typical issue is to get `NULL` from calling an image creation function (e.g. due to wrong image format). And this *needs* to be handled by the libgd client.

libwebp may fail to encode an image, and this also *needs* to be handled by the client; in this case libgd is the client.

The only reason `_gdImageGd()` may fail is due to one of the memory allocation functions failing (i.e. out of memory); this is indeed not cleanly handled by libgd (for a start, all putter functions in gd_io.c would need to report sucess/failure). However, this is not really an issue for me, since I mainly work with an infallible allocator.

---

Comment by vapier on 2021-05-26 21:29:24+00:00:
> libwebp may fail to encode an image, and this also needs to be handled by the client; in this case libgd is the client.

correct.  i would consider any library gd calls to be part of the "OS".

---

Comment by meweez on 2021-05-30 14:45:40+00:00:
Hi,
Similar to the previous discussion, function `_gdImageGd2`  which is called in gdImageGd2Ptr in gd_gd2.c there is no return value to show fail in `_gdImageGd2`  and no return value check in `gdImageGd2Ptr`. 

despite of `_gdImageGd()`, There are some conditions in `_gdImageGd2`  which lead it to `fail1`. So if it goes through these fail situations it means that `out` isn't allocated correctly and calling the `gdDPExtractData` leads to double free.

Isn't it necessary to add success and fail return value to `_gdImageGd2`?

---

Comment by cmb69 on 2021-05-31 13:40:13+00:00:
> Isn't it necessary to add success and fail return value to _gdImageGd2?

Oh, right. Stll, not a security issue, since it [is documented](https://libgd.github.io/manuals/2.3.1/files/gd_gd2-c.html):

> It has to be regarded as being obsolete, and should only be used for development and testing purposes.

---

Comment by meweez on 2021-05-31 21:14:07+00:00:
> not a security issue,

But there are some CVEs on `GD2 IO` like CVE-2016-3074 in `_gd2GetHeader` function.

---

Comment by cmb69 on 2021-05-31 21:23:43+00:00:
I guess the function was not documented to be not intended for production usage back then.

Build script:
#!/bin/bash -eu
# Minimized build script with only core build commands
set -eu
./bootstrap.sh
./configure --prefix="$WORK" --disable-shared
make -j$(nproc) install

Dockerfile:
FROM hwiwonlee/secb.base:latest
RUN apt-get update && apt-get install -y make autoconf automake libtool pkg-config libz-dev
RUN apt-get install -y make autoconf automake libtool pkg-config libz-dev
RUN git clone https://github.com/libgd/libgd libgd
RUN git -C libgd checkout b61fc2dbad5d1a8c0b4f0cbfa0076d9938f723d3
WORKDIR $SRC/libgd
COPY build.sh $SRC/

Additional files:
filename: gd_image_string_fuzzer.cc
content: // Copyright 2020 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
/////////////////////////////////////////////////////////////////////////////

#include <fuzzer/FuzzedDataProvider.h>

#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <string>

#include "gd.h"
#include "gdfontg.h"
#include "gdfontl.h"
#include "gdfontmb.h"
#include "gdfonts.h"
#include "gdfontt.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
    FuzzedDataProvider stream(data, size);
    const uint8_t slate_width = stream.ConsumeIntegral<uint8_t>();
    const uint8_t slate_height = stream.ConsumeIntegral<uint8_t>();
    gdImagePtr slate_image = gdImageCreateTrueColor(slate_width, slate_height);
    if (slate_image == nullptr) {
      return 0;
    }

    const int x_position = stream.ConsumeIntegral<int>();
    const int y_position = stream.ConsumeIntegral<int>();
    const int text_color = stream.ConsumeIntegral<int>();
    const gdFontPtr font_ptr = stream.PickValueInArray(
        {gdFontGetGiant(), gdFontGetLarge(), gdFontGetMediumBold(),
        gdFontGetSmall(), gdFontGetTiny()});
    const std::string text = stream.ConsumeRemainingBytesAsString();

    gdImageString(slate_image, font_ptr, x_position, y_position,
                  reinterpret_cast<uint8_t*>(const_cast<char*>(text.c_str())),
                  text_color);
    gdImageDestroy(slate_image);
    return 0;
}

filename: parser_target.cc
content: // Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
/////////////////////////////////////////////////////////////////////////////

#include <stdint.h>
#include "gd.h"

#define PASTE(x) gdImageCreateFrom ## x ## Ptr
#define CREATE_IMAGE(FORMAT) PASTE(FORMAT)

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
    gdImagePtr im = CREATE_IMAGE(FUZZ_GD_FORMAT)(Size, (void*) Data);
    if (im) gdImageDestroy(im);
    return 0;
}