Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Heap out-of-bounds read/write in gpredict transponder update (missing NUL after JSON load)

未关闭
#434 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
78/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
c, json

调研方向

Start with trsp_update_files() in src/trsp-update.c, then trace the length-unaware parsing and unescape logic in src/nxjson/nxjson.c; lab/nxjson_oob.c provides a standalone reproducer and build commands. Confirm both the exact-size JSON buffer behavior and the missing name path. Done means truncated strings, numbers, keywords, and escapes stay within bounds, malformed mode records without string fields are handled safely, and valid updates still parse.

由索引模型根据 Issue 内容生成。

描述

Summary

After the user runs Update transponders, gpredict writes the downloaded body to disk and parses it with nx_json_parse(). The file is loaded with g_malloc(file_size) and fread() of exactly file_size bytes. The buffer is not allocated as file_size + 1, and no trailing '\0' is written.

nx_json_parse() does not take a length. Strings, numbers, and the keywords true / false / null keep reading until they see a NUL or a character that ends the token. If the body is cut off inside one of those tokens and that cut sits on the end of the allocation, the parser reads the byte after the object. If that byte is a quote or the second byte of a backslash escape, it also writes past the object.

A complete document that already ends on ] does not do this. I checked that case on the same harness.

Startup does not fetch transponders. The request leaves certificate verification on and does not set CURLOPT_FOLLOWLOCATION. This build has no listener, and I did not find system, popen, or g_spawn on this path. The bug runs as the user who clicked the update. I did not obtain instruction-pointer control. The byte that gets written is taken from whatever already sits after the allocation.

The same function also crashes if a mode object has no "name" string. That one is a NULL dereference, described at the end.

What has to be true to hit it

trsp_update_from_network() (around line 685) calls trsp_update_files() only after a download returns a body. Default URLs, from src/sat-cfg.c lines 228–230:

  • https://db.satnogs.org/api/modes/?format=json
  • https://db.satnogs.org/api/transmitters/?format=json

The same parser runs on modes.json and transmitters.json already stored under the user config directory. The body has to be truncated inside a string, a number, or a keyword, with that token reaching the last byte of the file. A well-formed document is not enough.

Code

src/trsp-update.c, trsp_update_files(), modes file (transmitters is the same pattern at lines 247–258):

mfplen = flen;
rewind(mfp);
jsn_object = g_malloc(mfplen);
result = fread(jsn_object, mfplen, 1, mfp);
if (result == 1) {
    const nx_json *json = nx_json_parse(jsn_object, 0);

src/nxjson/nxjson.c:

  • unescape_string() line 104: while ((c = *p++)) with no remaining-length check
  • line 106: a " does *d = '\0' at whatever address d has reached
  • line 111: a \ reads one more byte; n / t / … then store a decoded byte through d
  • line 171: any other byte is copied with *d++ = c, which moves d forward even when p has already left the buffer
  • parse_value() line 284: strtoll(p, &pe, 0)
  • line 303: strncmp(p, "true", 4) (same for "false" / "null")

Local reproduction

I did not link GTK, so this is not a click-through of the gpredict window, and I did not send these bodies to db.satnogs.org. The parser binary is the repository file src/nxjson/nxjson.c with no edits. The driver copies the allocation in trsp_update_files(): malloc(n), memcpy of n bytes, nx_json_parse(buf, 0), no extra NUL.

Guard-page runs use the system allocator interposer:

/usr/lib/libgmalloc.dylib

libgmalloc places each malloc on its own page, so a read of buf[n] faults at the first address after the object. The write tests use mmap of two anonymous pages and put a 32-byte buffer on the last 32 bytes of the first page, so the next bytes are readable and writable. That stands in for a live heap neighbor. It is not a guess about glibc chunk layout.

Build
clang -O0 -g -std=c11 -I src/nxjson \
    lab/nxjson_oob.c src/nxjson/nxjson.c -o /tmp/nxjson_oob

arm64 write checks:

clang -arch arm64 -O2 -g -std=c11 -I src/nxjson \
    lab/nxjson_oob.c src/nxjson/nxjson.c -o /tmp/nxjson_oob_arm
1. Control — complete document, no read past the object

Bytes loaded into malloc(9) (this is the whole buffer; nothing follows inside the allocation):

0000  5b 7b 22 61 22 3a 31 7d 5d                       [{"a":1}]
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib /tmp/nxjson_oob control
control malloc(9) parse=object

Exit status 0. The parser returned an object and did not fault.

2. Unclosed string — read of the first byte after the object

48-byte body. First 7 bytes are the token start, the other 41 bytes are 0x41. There is no closing quote inside the allocation:

0000  5b 7b 22 64 22 3a 22 41 41 41 41 41 41 41 41 41  [{"d":"AAAAAAAAA
0010  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
0020  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib /tmp/nxjson_oob string
string malloc(48) FAULT sig=11 addr=0x10bd94000 buf=0x10bd93fd0 end=0x10bd94000

addr - end = 0. 0x10bd93fd0 + 48 = 0x10bd94000. SIGSEGV is raised on the first byte the allocator did not hand out.

3. Truncated number

11-byte body, last byte is the digit 5, no terminator inside the object:

0000  5b 7b 22 61 22 3a 31 32 33 34 35                 [{"a":12345

strtoll() walks past that digit. Under the same libgmalloc setup the fault address minus the end of the buffer is 0.

4. Truncated keyword

10-byte body. The last in-bounds bytes are t r u. strncmp(p, "true", 4) still compares 4 bytes:

0000  5b 7b 22 61 22 3a 74 72 75                       [{"a":tru

Same allocator, same result: fault address equals the first byte after the allocation.

5. Out-of-bounds write — the byte after the object is a quote

32-byte buffer parked at the end of a page (mmap, next page left PROT_READ|PROT_WRITE). In-bounds bytes:

0000  5b 7b 22 64 22 3a 22 41 41 41 41 41 41 41 41 41  [{"d":"AAAAAAAAA
0010  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA

Bytes I placed on the next page before nx_json_parse():

offset 32  22 5d 7d 00                                 "]}

unescape_string() treats offset 32 as the closing quote and executes *d = '\0' there.

/tmp/nxjson_oob quote
quote before neighbor[0]=0x22
quote after  neighbor[0]=0x00

0x22 was stored as 0x00. Repeated with clang -arch arm64 -O2. Same store.

6. Out-of-bounds write — last in-bounds byte is a backslash

Same 32-byte page-end buffer. Byte 31 (last in-bounds) set to 0x5c. Next page before the call:

offset 31  5c                                          \
offset 32  6e 58 00                                    nX

The \ is in bounds. The n and the following X are not. Handling the escape consumes n and then the normal-character arm copies the next byte back through d, which has already moved onto the next page.

/tmp/nxjson_oob escape
escape before neighbor[0]=0x6e neighbor[1]=0x58
escape after  neighbor[0]=0x58 neighbor[1]=0x58

neighbor[0] changed from 0x6e (n) to 0x58 (X). The parser error report also printed those XX bytes, so the %s used for the error walks off the allocation as well. Same result with arm64 -O2.

The value written is whatever byte already followed the buffer (0x22 becomes 0x00, 0x6e becomes the following 0x58). It is not an arbitrary byte taken from the JSON text, and this run did not redirect the instruction pointer. On a guard page the same inputs crash instead of writing. On a live heap the neighbor is whatever g_malloc happened to place next.

Driver

lab/nxjson_oob.c, compiled against the untouched src/nxjson/nxjson.c from that commit. The three entries below are the ones that produced the logs above.

static int parse_exact(const char *bytes, size_t n, const char *tag)
{
    char *buf = malloc(n);          /* same shape as g_malloc(mfplen) */
    memcpy(buf, bytes, n);          /* no buf[n] = 0 */
    if (sigsetjmp(jb, 1) == 0) {
        const nx_json *js = nx_json_parse(buf, 0);
        printf("%s malloc(%zu) parse=%s\n", tag, n, js ? "object" : "NULL");
        ...
    }
    printf("%s malloc(%zu) FAULT sig=%d addr=%p buf=%p end=%p\n",
           tag, n, fault_sig, fault_addr, (void *)buf, (void *)(buf + n));
}

/* quote: 32-byte buffer at end of page, next byte is 0x22 */
after[0] = '"'; after[1] = ']'; after[2] = '}'; after[3] = 0;
nx_json_parse(buf, 0);   /* after[0] becomes 0x00 */

/* escape: last in-bounds byte is '\\', next bytes are 'n','X' */
buf[n - 1] = '\\';
after[0] = 'n'; after[1] = 'X'; after[2] = 0;
nx_json_parse(buf, 0);   /* after[0] becomes 0x58 */

Related crash in the same function

nx_json_get() (lines 364–370) returns a static dummy when the key is absent. dummy is static const nx_json dummy = { NX_JSON_NULL }, so text_value is NULL. It does not return a C null pointer. The caller still does:

strncpy(m_modes.name, nx_json_get(json_obj, "name")->text_value, 79);

description at line 271 is the same call. A complete document is enough:

0000  5b 7b 22 69 64 22 3a 31 7d 5d                    [{"id":1}]
char raw[] = "[{\"id\":1}]";
const nx_json *name = nx_json_get(nx_json_item(nx_json_parse(raw, 0), 0), "name");
char dst[80];
strncpy(dst, name->text_value, 79);   /* text_value == NULL */

Built with clang -arch arm64 -O2. The disassembly still contains bl _strncpy. Process exit status 139. This is a crash at address 0, not a write of attacker-chosen data.

Suggested fix

  • Allocate mfplen + 1 and set the last byte to 0 before nx_json_parse().
  • Before each strncpy, require a string node whose text_value is non-NULL. Drop the record otherwise. Apply that to both name and description.
  • Give nxjson the caller’s end pointer, and stop unescape_string, strtoll, and strncmp at that end.

Credit

Found and reproduced locally against commit da7243fe7aaa14b20782d127b0fa5bd3c21e2dfe. The logs above are from that run.

主要语言
C
星标
1.2k
派生
305
平均合并
8 小时 48 分钟
30 天内合并 PR
3

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

csete/gpredict 的其他 Issue

查看 csete/gpredict 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。