Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions devel/214_10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# [214_10]

## 任务相关的代码文件
- src/goldfish.hpp
- tests/goldfish/liii/njson-test.scm
- devel/214_10.md

## 如何测试
```bash
xmake b goldfish
bin/goldfish tests/goldfish/liii/njson-test.scm
bin/goldfish bench/njson-bench.scm
```

## 2026/3/2 修复 njson 句柄安全、set 写入语义与数值边界问题

### What
1. 修复句柄安全漏洞:
- 句柄从 `(njson-handle . id)` 升级为 `(njson-handle . (id . generation))`;
- 新增 `njson_handle_generations` 代际表,句柄校验必须同时匹配 `id + generation`;
- free-list 复用槽位时,`generation` 自增,旧句柄自动失效。
2. 修复 `set/set!` 在“目标 parent 为标量”时静默 no-op 的问题:
- object/array 上原有行为保持不变(object upsert、array 仅允许已存在索引覆盖);
- 当根或中间路径命中标量时,不再“成功返回但不生效”;
- 非容器 parent 现在统一抛 `key-error`;
- 错误消息:`set target must be array or object`。
3. 收紧数值输入语义:
- 拒绝非有限实数(`NaN/+Inf/-Inf`);
- 拒绝复数等“非 real number”输入;
- 防止“内存是 number,序列化变 null”的静默漂移。
4. 测试增强:
- 增加 stale handle/伪造 handle/重复 free 防回归用例;
- 增加 `njson->string`、`set/set!`、`append/append!` 对 `NaN/Inf/complex` 的错误断言;
- 同步更新测试注释中的句柄结构说明。

### Why
1. 仅靠 `id` 的句柄在槽位复用时会产生“旧句柄复活”问题,进一步导致误释放新对象,存在明显安全风险。
2. `set/set!` 对标量 parent 的静默成功会造成“写入看似成功、实际无变化”的数据一致性风险,排查成本高。
3. JSON 标准不接受 `NaN/Inf`,若输入端接受而输出端转换为 `null`,会造成难以发现的语义漂移。
4. 需要让 njson 的错误模型在句柄生命周期、路径写入和数值边界上都保持可预测、可测试、可回归。

### How
1. 句柄代际机制:
- `make_njson_handle` 输出 `(tag . (id . generation))`;
- `is_njson_handle`/`extract_njson_handle_id` 解析并校验 generation;
- 校验失败时明确区分:非法 generation、句柄不存在、stale handle mismatch。
2. 槽位复用逻辑:
- 新增 `njson_ensure_generations_size`;
- `store_njson_value` 在复用 free id 时先递增 generation,再写入 JSON;
- 新分配槽位 generation 初始化为 `1`。
3. `set/set!` 非容器 parent 行为修正:
- 在 `njson_apply_update_on_root` 的 `set` 分支补充非容器错误返回,避免 no-op;
- 覆盖两类场景:`(set scalar-root key value)` 与 `(set obj \"k\" \"x\" value)`(其中 `obj.k` 为标量)。
4. 数值边界修正:
- `scheme_to_njson_scalar_or_handle` 区分 `real` 与 `number`;
- `real` 需 `std::isfinite`;
- 非 real 的 number 直接拒绝。
5. 测试层回归:
- 新增 stale-handle 与 forged-handle 负例;
- 新增 finite number 约束的错误类型+错误消息断言;
- 保证行为变更均由测试锁定。

### 兼容性说明
1. 句柄内部形态变化:从 `(njson-handle . id)` 变为 `(njson-handle . (id . generation))`。
2. 历史句柄在槽位复用后不再可用,会报 `generation mismatch (stale handle)`。
3. `(cons 'njson-handle 1)` 这类旧伪造结构不再可通过句柄校验。
4. `njson-set` / `njson-set!` 对标量 parent 现在会抛 `key-error`,不再静默 no-op。
- 典型错误消息:`g_njson-set: set target must be array or object`、`g_njson-set!: set target must be array or object`。
5. `njson->string`、`set/set!`、`append/append!` 对 `NaN/Inf/complex` 输入会抛 `type-error`。

## 验证结果
本次文档编写前已执行:
1. ✅ `xmake b goldfish` 通过。
2. ✅ `bin/goldfish tests/goldfish/liii/njson-test.scm` 通过(`; *** checks *** : 336 correct, 0 failed.`)。
3. ✅ `bin/goldfish bench/njson-bench.scm` 通过(`; *** checks *** : 29 correct, 0 failed.`)。
77 changes: 68 additions & 9 deletions src/goldfish.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <algorithm>
#include <argh.h>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
Expand Down Expand Up @@ -87,6 +88,7 @@ glue_define (s7_scheme* sc, const char* name, const char* desc, s7_function f, s

static const char* NJSON_HANDLE_TAG = "njson-handle";
static std::vector<std::unique_ptr<json>> njson_handle_store = std::vector<std::unique_ptr<json>> (1);
static std::vector<s7_int> njson_handle_generations = std::vector<s7_int> (1, 0);
static std::vector<s7_int> njson_handle_free_ids;
static std::vector<s7_pointer> njson_keys_cache_values = std::vector<s7_pointer> (1, nullptr);
static std::vector<s7_int> njson_keys_cache_gc_locs = std::vector<s7_int> (1, -1);
Expand All @@ -111,30 +113,57 @@ njson_error (s7_scheme* sc, const char* type_name, const std::string& msg, s7_po

static s7_pointer
make_njson_handle (s7_scheme* sc, s7_int id) {
return s7_cons (sc, s7_make_symbol (sc, NJSON_HANDLE_TAG), s7_make_integer (sc, id));
s7_int generation = 0;
if (id > 0) {
size_t index = static_cast<size_t> (id);
if (index < njson_handle_generations.size ()) {
generation = njson_handle_generations[index];
}
}
return s7_cons (
sc, s7_make_symbol (sc, NJSON_HANDLE_TAG), s7_cons (sc, s7_make_integer (sc, id), s7_make_integer (sc, generation)));
}

static bool
is_njson_handle (s7_pointer x) {
is_njson_handle (s7_pointer x, s7_int* id_out = nullptr, s7_int* generation_out = nullptr) {
if (!s7_is_pair (x)) return false;
s7_pointer tag = s7_car (x);
s7_pointer id = s7_cdr (x);
s7_pointer payload = s7_cdr (x);
if (!s7_is_symbol (tag)) return false;
if (strcmp (s7_symbol_name (tag), NJSON_HANDLE_TAG) != 0) return false;
return s7_is_integer (id);
if (!s7_is_pair (payload)) return false;
s7_pointer id = s7_car (payload);
s7_pointer generation = s7_cdr (payload);
if (!s7_is_integer (id) || !s7_is_integer (generation)) return false;
if (id_out) *id_out = s7_integer (id);
if (generation_out) *generation_out = s7_integer (generation);
return true;
}

static bool
extract_njson_handle_id (s7_scheme* sc, s7_pointer handle, s7_int& id, std::string& error_msg) {
if (!is_njson_handle (handle)) {
s7_int generation = 0;
if (!is_njson_handle (handle, &id, &generation)) {
error_msg = "expected njson handle";
return false;
}
id = s7_integer (s7_cdr (handle));
if (id <= 0) {
error_msg = "invalid njson handle id";
return false;
}
if (generation <= 0) {
error_msg = "invalid njson handle generation";
return false;
}
size_t index = static_cast<size_t> (id);
if (index >= njson_handle_generations.size ()) {
error_msg = "njson handle does not exist (may have been freed)";
return false;
}
if (njson_handle_generations[index] != generation) {
error_msg = "njson handle generation mismatch (stale handle)";
return false;
}
if (static_cast<size_t> (id) >= njson_handle_store.size () || !njson_handle_store[static_cast<size_t> (id)]) {
error_msg = "njson handle does not exist (may have been freed)";
return false;
Expand Down Expand Up @@ -167,6 +196,13 @@ njson_ensure_keys_cache_size (size_t n) {
}
}

static void
njson_ensure_generations_size (size_t n) {
if (njson_handle_generations.size () < n) {
njson_handle_generations.resize (n, 0);
}
}

static void
njson_clear_keys_cache_slot (s7_scheme* sc, s7_int id) {
if (id <= 0) return;
Expand Down Expand Up @@ -230,12 +266,23 @@ store_njson_value (json&& value) {
if (!njson_handle_free_ids.empty ()) {
s7_int id = njson_handle_free_ids.back ();
njson_handle_free_ids.pop_back ();
njson_handle_store[static_cast<size_t> (id)] = std::make_unique<json> (std::move (value));
size_t index = static_cast<size_t> (id);
njson_ensure_generations_size (index + 1);
s7_int generation = njson_handle_generations[index];
if (generation <= 0 || generation == (std::numeric_limits<s7_int>::max) ()) {
generation = 1;
}
else {
generation += 1;
}
njson_handle_generations[index] = generation;
njson_handle_store[index] = std::make_unique<json> (std::move (value));
njson_ensure_keys_cache_size (njson_handle_store.size ());
return id;
}

njson_handle_store.push_back (std::make_unique<json> (std::move (value)));
njson_handle_generations.push_back (1);
njson_ensure_keys_cache_size (njson_handle_store.size ());
s7_int id = static_cast<s7_int> (njson_handle_store.size () - 1);
return id;
Expand Down Expand Up @@ -381,10 +428,19 @@ scheme_to_njson_scalar_or_handle (s7_scheme* sc, s7_pointer value, json& out, st
out = static_cast<long long> (s7_integer (value));
return true;
}
if (s7_is_real (value) || s7_is_number (value)) {
out = s7_number_to_real (sc, value);
if (s7_is_real (value)) {
double real_value = s7_number_to_real (sc, value);
if (!std::isfinite (real_value)) {
error_msg = "number must be finite (NaN/Inf are not valid JSON numbers)";
return false;
}
out = real_value;
return true;
}
if (s7_is_number (value)) {
error_msg = "number must be real and finite";
return false;
}
if (s7_is_symbol (value)) {
const char* symbol_name = s7_symbol_name (value);
if (strcmp (symbol_name, "null") == 0) {
Expand Down Expand Up @@ -795,6 +851,9 @@ njson_apply_update_on_root (s7_scheme* sc, json& root, const std::vector<s7_poin
return njson_error (sc, "key-error", std::string (api_name) + ": path not found: cannot drop from non-container value",
last_key);
}
if (op == njson_update_op::set) {
return njson_error (sc, "key-error", std::string (api_name) + ": set target must be array or object", last_key);
}
return nullptr;
}

Expand Down
77 changes: 76 additions & 1 deletion tests/goldfish/liii/njson-test.scm
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@
(car payload)
"")))))

(define (capture-type-error-message thunk)
(catch 'type-error
thunk
(lambda args
(let ((payload (if (and (pair? args) (pair? (cdr args))) (cadr args) '())))
(if (and (pair? payload) (string? (car payload)))
(car payload)
"")))))

#|
let-njson
统一处理“可能是 njson 句柄,也可能是普通标量”的作用域宏。
Expand Down Expand Up @@ -221,6 +230,26 @@ json-string : string
(check-catch 'type-error (njson-ref njson-string-free-check "x"))
(check-catch 'type-error (njson-free 'foo))

(define stale-handle-old (string->njson "{\"a\":1}"))
(check (njson-ref stale-handle-old "a") => 1)
(check-true (njson-free stale-handle-old))
(let-njson ((stale-handle-new (string->njson "{\"b\":2}")))
(check (njson-ref stale-handle-new "b") => 2)
(check-catch 'type-error (njson-ref stale-handle-old "b"))
(check-catch 'type-error (njson-free stale-handle-old))
;; stale free must not affect the new handle if id is reused.
(check (njson-ref stale-handle-new "b") => 2))

;; Old forged shape `(njson-handle . id)` must be rejected.
(check-catch 'type-error (njson-ref (cons 'njson-handle 1) "x"))
;; Forged generation must be rejected.
(let-njson ((root (string->njson "{\"secret\":42}")))
(let* ((payload (cdr root))
(id (car payload))
(gen (cdr payload))
(forged (cons 'njson-handle (cons id (+ gen 1)))))
(check-catch 'type-error (njson-ref forged "secret"))))

#|
njson?
判断值是否为 njson-handle。
Expand All @@ -236,7 +265,7 @@ x : any

行为逻辑
--------
1. 仅检查结构是否符合 njson 句柄格式(`(njson-handle . id)`)。
1. 仅检查结构是否符合 njson 句柄格式(`(njson-handle . (id . generation))`)。
2. 不负责判断该句柄是否已释放;“已释放”通常在具体 API 调用时触发错误。

返回值
Expand Down Expand Up @@ -430,6 +459,12 @@ value : njson-handle | string | number | boolean | 'null
(check-catch 'type-error (njson-set 'foo "meta" "os" "debian"))
(let-njson ((root (string->njson sample-json)))
(check-catch 'key-error (njson-set root 'meta "os" "debian")))
(let-njson ((root (string->njson sample-json)))
(check-catch 'type-error (njson-set root "score" +nan.0))
(check-catch 'type-error (njson-set root "score" +inf.0))
(check-catch 'type-error (njson-set root "score" -inf.0))
(check (capture-type-error-message (lambda () (njson-set root "score" +nan.0)))
=> "g_njson-set: number must be finite (NaN/Inf are not valid JSON numbers)"))
(let-njson ((root (string->njson sample-json)))
(check-catch 'key-error (njson-set root "nums" 5 1)))
(let-njson ((root (string->njson sample-json)))
Expand All @@ -441,6 +476,14 @@ value : njson-handle | string | number | boolean | 'null
(let-njson ((root (string->njson sample-json)))
(check (capture-key-error-message (lambda () (njson-set root "nums" 5 1)))
=> "g_njson-set: array index out of range (index=5, size=5)"))
(let-njson ((root (string->njson "1")))
(check-catch 'key-error (njson-set root "x" 1))
(check (capture-key-error-message (lambda () (njson-set root "x" 1)))
=> "g_njson-set: set target must be array or object"))
(let-njson ((root (string->njson sample-json)))
(check-catch 'key-error (njson-set root "name" "x" "y"))
(check (capture-key-error-message (lambda () (njson-set root "name" "x" "y")))
=> "g_njson-set: set target must be array or object"))

#|
njson-set!
Expand Down Expand Up @@ -494,6 +537,12 @@ value : njson-handle | string | number | boolean | 'null
(check-catch 'type-error (njson-set! 'foo "meta" "os" "debian"))
(let-njson ((root (string->njson sample-json)))
(check-catch 'key-error (njson-set! root 'meta "os" "debian")))
(let-njson ((root (string->njson sample-json)))
(check-catch 'type-error (njson-set! root "score" +nan.0))
(check-catch 'type-error (njson-set! root "score" +inf.0))
(check-catch 'type-error (njson-set! root "score" -inf.0))
(check (capture-type-error-message (lambda () (njson-set! root "score" +nan.0)))
=> "g_njson-set!: number must be finite (NaN/Inf are not valid JSON numbers)"))
(let-njson ((root (string->njson sample-json)))
(check-catch 'key-error (njson-set! root "nums" 5 1)))
(let-njson ((root (string->njson sample-json)))
Expand All @@ -502,6 +551,14 @@ value : njson-handle | string | number | boolean | 'null
=> "g_njson-set!: path not found: missing object key 'missing'"))
(let-njson ((root (string->njson sample-json)))
(check-catch 'key-error (njson-set! root "nums" 999 1)))
(let-njson ((root (string->njson "1")))
(check-catch 'key-error (njson-set! root "x" 1))
(check (capture-key-error-message (lambda () (njson-set! root "x" 1)))
=> "g_njson-set!: set target must be array or object"))
(let-njson ((root (string->njson sample-json)))
(check-catch 'key-error (njson-set! root "name" "x" "y"))
(check (capture-key-error-message (lambda () (njson-set! root "name" "x" "y")))
=> "g_njson-set!: set target must be array or object"))

#|
njson-append
Expand Down Expand Up @@ -550,6 +607,11 @@ value : njson-handle | string | number | boolean | 'null

(check-catch 'type-error (njson-append 'foo 1))
(let-njson ((root (string->njson sample-json)))
(check-catch 'type-error (njson-append root "nums" +nan.0))
(check-catch 'type-error (njson-append root "nums" +inf.0))
(check-catch 'type-error (njson-append root "nums" -inf.0))
(check (capture-type-error-message (lambda () (njson-append root "nums" +nan.0)))
=> "g_njson-append: number must be finite (NaN/Inf are not valid JSON numbers)")
(check-catch 'key-error (njson-append root))
(check-catch 'key-error (njson-append root "as"))
(check (capture-key-error-message (lambda () (njson-append root "as")))
Expand Down Expand Up @@ -606,6 +668,11 @@ value : njson-handle | string | number | boolean | 'null

(check-catch 'type-error (njson-append! 'foo 1))
(let-njson ((root (string->njson sample-json)))
(check-catch 'type-error (njson-append! root "nums" +nan.0))
(check-catch 'type-error (njson-append! root "nums" +inf.0))
(check-catch 'type-error (njson-append! root "nums" -inf.0))
(check (capture-type-error-message (lambda () (njson-append! root "nums" +nan.0)))
=> "g_njson-append!: number must be finite (NaN/Inf are not valid JSON numbers)")
(check-catch 'key-error (njson-append! root))
(check-catch 'key-error (njson-append! root "as"))
(check (capture-key-error-message (lambda () (njson-append! root "as")))
Expand Down Expand Up @@ -1006,6 +1073,14 @@ value : njson-handle | string | number | boolean | 'null
(check (njson->string #f) => "false")
(let-njson ((root (string->njson "{\"b\":1,\"a\":2}")))
(check (njson->string root) => "{\"a\":2,\"b\":1}"))
(check-catch 'type-error (njson->string +nan.0))
(check-catch 'type-error (njson->string +inf.0))
(check-catch 'type-error (njson->string -inf.0))
(check-catch 'type-error (njson->string 1+2i))
(check (capture-type-error-message (lambda () (njson->string +nan.0)))
=> "g_njson-json->string: number must be finite (NaN/Inf are not valid JSON numbers)")
(check (capture-type-error-message (lambda () (njson->string 1+2i)))
=> "g_njson-json->string: number must be real and finite")
(check-catch 'type-error (njson->string 'foo))

(define njson-string-freed (string->njson "{\"k\":1}"))
Expand Down