[BUG] cJSON_PrintBuffered(item, 0, ...) with custom hooks: ensure() reads 1 byte from a zero-sized buffer (memcpy(..., p->offset + 1))

Abierto Apto para principiantes
#1,122 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 12 días

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
70/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
c
Área
backend

Línea de trabajo

Empieza con ensure() en cJSON.c, en la rama de reasignación manual descrita en el issue, y luego sigue cómo cJSON_PrintBuffered() inicializa el búfer cuando prebuffer es cero. Reproduce el problema con el asignador adjunto y compara los valores 0 y 1 de prebuffer. El trabajo estará terminado cuando se manejen los búferes de tamaño cero sin lecturas fuera de los límites y la reproducción imprima la cadena correctamente.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Summary

cJSON_PrintBuffered() accepts prebuffer == 0 (only negative values are rejected). With hooks installed via cJSON_InitHooks() that do not provide reallocate (i.e. the manual-reallocation branch of ensure()), the first growth does

memcpy(newbuffer, p->buffer, p->offset + 1);   /* cJSON.c:523 in v1.7.18 */

with p->length == 0 and p->offset == 0, i.e. it reads 1 byte from an object whose size is 0. This is an out-of-bounds read (undefined behaviour).

Affected code

  • cJSON_PrintBuffered(): prebuffer < 0 is rejected, 0 is passed to global_hooks.allocate(0); p.length = 0.
  • ensure(): the (p->length > 0) && ... guard is skipped for length 0, then the manual branch copies offset + 1 bytes from the old buffer, which has length == 0.

Not affected: default hooks (malloc/free -> realloc branch), and allocators returning NULL for allocate(0) (function returns early).

Why this was not noticed earlier

Per the C standard (ISO C99 §7.20.3 / C11 §7.22.3), a zero-sized allocation request has implementation-defined behaviour: the allocator may either return a null pointer, or return a non-null pointer with the explicit requirement that "the returned pointer shall not be used to access an object." Even when allocate(0) returns a non-NULL pointer, accessing any byte through it is illegal.

In practice, common runtime environments masked this out-of-bounds read in different ways:

  • Linux / glibc (ptmalloc) has a minimum allocation unit: When custom hooks wrapping malloc/free are installed on Linux (entering the manual memcpy branch), malloc(0) rounds the request up to the minimum heap chunk size (MINSIZE, typically 32 bytes on 64-bit targets with 16–24 bytes of usable payload). As a result, memcpy(newbuffer, p->buffer, 1) reads 1 uninitialised byte from inside that minimum chunk without faulting, and that byte is immediately overwritten by the subsequent print routine.
  • FreeRTOS (pvPortMalloc) returns NULL (0) for 0-byte requests: Standard FreeRTOS heap implementations (heap_1, heap_2, heap_4, heap_5) guard allocation with if (xWantedSize > 0) and return NULL when given 0. Consequently, cJSON_PrintBuffered() hits if (!p.buffer) return NULL; and exits before ever reaching ensure().
  • Default hooks use realloc: Without cJSON_InitHooks(), ensure() uses realloc() rather than the manual allocate + memcpy + deallocate path.

However, under any allocator that returns a non-dereferenceable non-null pointer for size 0 (such as sentinel-pointer allocators like ZERO_SIZE_PTR, slab/pool allocators returning an end-of-page boundary, or guard-page/hardened allocators), reading 1 byte from the 0-sized buffer immediately faults.

Reproduction

Self-contained repro.c (attached below). The custom allocator returns, for a 0-byte request, a pointer to the start of an inaccessible page, so the out-of-bounds read becomes observable. This only makes the UB visible; it is a legal return value for malloc(0)-style allocators.

gcc -g -I<cJSON> repro.c <cJSON>/cJSON.c -lm -o repro
./repro 1    # prebuffer=1 -> "abc"
./repro 0    # Segmentation fault

gdb backtrace (v1.7.18):

#1 ensure (p=..., needed=7) at cJSON.c:523
#2 print_string_ptr (input="abc", ...) at cJSON.c:964
#3 print_string ...
#4 print_value ...
#5 cJSON_PrintBuffered (item=..., prebuffer=0, fmt=0) at cJSON.c:1295

Control: prebuffer = 1 prints "abc" correctly. Same 0-crash / 1-ok result observed on v1.7.11, v1.7.18 and master at v1.7.19-6-g6d9f244.

Note: AddressSanitizer does not report this with a system malloc(0)-backed allocator, because glibc/ASan hand out a >= 1 byte block for size 0, so reading byte 0 stays in bounds. The bug is the missing guarantee, not the specific crash.

Discovery

Found during AI-assisted analysis of the compiled cJSON binary (model-proposed hypothesis from disassembly: "ensure() may copy one byte too many when length is 0"); the public-API trigger and the allocator above were constructed and verified manually.

Attachment: repro.c

repro.c

/* gcc -g -I<cJSON dir> repro.c <cJSON dir>/cJSON.c -lm -o repro ; ./repro 0 ; ./repro 1 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include "cJSON.h"

/* Allocator that returns a pointer to an inaccessible page for a 0-byte request,
   making the out-of-bounds read of a zero-sized object observable. */
static void *zero_block;
static void *my_malloc(size_t n)
{
    if (n == 0) {
        long pg = sysconf(_SC_PAGESIZE);
        char *m = mmap(NULL, 2 * pg, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        mprotect(m + pg, pg, PROT_NONE);
        return zero_block = m + pg;
    }
    return malloc(n);
}
static void my_free(void *p) { if (p != zero_block) free(p); }

int main(int argc, char **argv)
{
    int prebuffer = argc > 1 ? atoi(argv[1]) : 0;
    cJSON_Hooks h = { my_malloc, my_free };
    cJSON_InitHooks(&h);
    cJSON *s = cJSON_CreateString("abc");
    char *out = cJSON_PrintBuffered(s, prebuffer, 0);
    printf("prebuffer=%d -> %s\n", prebuffer, out ? out : "NULL");
    my_free(out);
    cJSON_Delete(s);
    return 0;
}
Lenguaje dominante
C
Estrellas
13.1k
Forks
3.6k
Métricas de merge de PR
Sin PR fusionados en 30 d

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de DaveGamble/cJSON

Todos los issues de DaveGamble/cJSON

Issues similares

Más issues de C

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.