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

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

Open
JarLob opened this issue Oct 19, 2023 · 0 comments · May be fixed by #1549
Open

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

JarLob opened this issue Oct 19, 2023 · 0 comments · May be fixed by #1549

Comments

@JarLob
Copy link

JarLob commented Oct 19, 2023

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
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

Successfully merging a pull request may close this issue.

1 participant