zip Buffer Overflow Detected with Multibyte Filename and Fortified glibc

Posted on


How to ReproduceHow It HappenedSuggested FixConclusion

#How to Reproduce

Compile a zip on Linux with gcc 12 or newer, optimization O1 or more, and -D_FORTIFY_SOURCE=3. Create a file with a multibyte filename, and try to compress it. For example:

$ touch 汉字
$ LC_CTYPE="en_US.UTF-8" ./zip 汉字
*** buffer overflow detected ***: terminated

zip error: Interrupted (aborting)

#How It Happened

When Unicode support is enabled, zip processes file names with the function local_to_wide_string. The function calculates the size of the converted wide string, allocates a buffer with that size, and puts the converted string into the buffer, as is shown in the following simplified code:

zwchar *local_to_wide_string(local_string)
  char *local_string;
  int wsize = mbstowcs(NULL, local_string, MB_CUR_MAX );
  wchar_t *wc_string = (wchar_t *)malloc((wsize + 1) * sizeof(wchar_t));
  wsize = mbstowcs(wc_string, local_string, strlen(local_string) + 1);
  // rest of function body omitted

Recent versions of glibc provides a FORTIFY_SOURCE option that enables additional parameter checks for certain functions, including mbstowcs. gcc 12 provides a builtin_dynamic_object_size that returns the size of a dynamically allocated object. Recall that for mbstowcs, the third parameter marks the maximum number of wide chars written to the destination array. When builtin_dynamic_object_size is available, fortified mbstowcs will check whether the third parameter is no more than the dynamic size of the first parameter and report buffer overflow detected when not.

Let's come back to local_to_wide_string function. When the input local_string contains a multibyte character, in the second mbstowcs call, the third parameter is strlen(local_string) + 1, but the destination wc_string is only allocated of size wsize + 1, thus failed to pass the fortified mbstowcs check.

Note that no actual buffer overflow would happen because there are only wsize elements that can be written to wc_string.

#Suggested Fix

diff --git a/fileio.c b/fileio.c
index 1847e62..5a2959d 100644
--- a/fileio.c
+++ b/fileio.c
@@ -3502,7 +3502,7 @@ zwchar *local_to_wide_string(local_string)
   if ((wc_string = (wchar_t *)malloc((wsize + 1) * sizeof(wchar_t))) == NULL) {
     ZIPERR(ZE_MEM, "local_to_wide_string");
-  wsize = mbstowcs(wc_string, local_string, strlen(local_string) + 1);
+  wsize = mbstowcs(wc_string, local_string, wsize + 1);
   wc_string[wsize] = (wchar_t) 0;
   /* in case wchar_t is not zwchar */


This bug was reported to Info-ZIP.

But hey dude, why bother using zip?