Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

オープン 初心者向け
#1,122 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 12 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
70/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
c
領域
backend

調査の方向性

まず、issueで説明されている手動再割り当て分岐の周辺にある ensure() を cJSON.c で確認し、次に prebuffer がゼロの場合に cJSON_PrintBuffered() がバッファをどう初期化するかを追ってください。添付されたアロケータで再現し、prebuffer の値が0の場合と1の場合を比較してください。ゼロサイズのバッファが範囲外読み取りなしで処理され、再現時に文字列が正しく出力されれば完了です。

索引モデルが issue の本文から書いたものです。

説明

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;
}
主要言語
C
スター
13.1k
フォーク
3.6k
PR マージ指標
30日以内にマージされた PR はありません

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

DaveGamble/cJSON のほかの issue

DaveGamble/cJSON の issue をすべて見る

似ている issue

C の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。