[BUG] cJSON_PrintBuffered(item, 0, ...) with custom hooks: ensure() reads 1 byte from a zero-sized buffer (memcpy(..., p->offset + 1))
メンテナーはふだん 12 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
まず、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 < 0is rejected,0is passed toglobal_hooks.allocate(0);p.length = 0.ensure(): the(p->length > 0) && ...guard is skipped for length 0, then the manual branch copiesoffset + 1bytes from the old buffer, which haslength == 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 wrappingmalloc/freeare installed on Linux (entering the manualmemcpybranch),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) returnsNULL(0) for 0-byte requests: Standard FreeRTOS heap implementations (heap_1,heap_2,heap_4,heap_5) guard allocation withif (xWantedSize > 0)and returnNULLwhen given0. Consequently,cJSON_PrintBuffered()hitsif (!p.buffer) return NULL;and exits before ever reachingensure(). - Default hooks use
realloc: WithoutcJSON_InitHooks(),ensure()usesrealloc()rather than the manualallocate + memcpy + deallocatepath.
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
/* 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 はありません
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
DaveGamble/cJSON のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
DaveGamble/cJSON#1106 ·
メンテナーはふだん 12 日以内に返信
-
Undefined-Behavior in function cJSON_SetNumberHelper対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
DaveGamble/cJSON#1100 · リアクション 1 件 ·
メンテナーはふだん 12 日以内に返信
-
CMake config files contain absolute install paths対応中かも @akashdeshmukh-git が 7 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
DaveGamble/cJSON#1094 · コメント 9 件 · リアクション 1 件 ·
メンテナーはふだん 12 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
DaveGamble/cJSON#1093 ·
メンテナーはふだん 12 日以内に返信
-
Use-after-free in cJSONUtils_ApplyPatches when a patch removes the patch array対応中かも @iliasabk が 42 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
DaveGamble/cJSON#1082 ·
メンテナーはふだん 12 日以内に返信
DaveGamble/cJSON の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
ly05010419/MacNativeGames#1420 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
JustAResearcher/gpu-unlock-ada#124 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
swoole/swoole-cli#1092 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
-
inflate: total_in uses plain += on c_ulong (32-bit on Windows), panics in debug for streams > 4 GiBオープン
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
trifectatechfoundation/zlib-rs#570 · コメント 1 件 ·