diff --git a/devel/214_10.md b/devel/214_10.md new file mode 100644 index 00000000..eb099fe1 --- /dev/null +++ b/devel/214_10.md @@ -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.`)。 diff --git a/src/goldfish.hpp b/src/goldfish.hpp index cc4326bc..76cf8b26 100644 --- a/src/goldfish.hpp +++ b/src/goldfish.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -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> njson_handle_store = std::vector> (1); +static std::vector njson_handle_generations = std::vector (1, 0); static std::vector njson_handle_free_ids; static std::vector njson_keys_cache_values = std::vector (1, nullptr); static std::vector njson_keys_cache_gc_locs = std::vector (1, -1); @@ -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 (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 (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 (id) >= njson_handle_store.size () || !njson_handle_store[static_cast (id)]) { error_msg = "njson handle does not exist (may have been freed)"; return false; @@ -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; @@ -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 (id)] = std::make_unique (std::move (value)); + size_t index = static_cast (id); + njson_ensure_generations_size (index + 1); + s7_int generation = njson_handle_generations[index]; + if (generation <= 0 || generation == (std::numeric_limits::max) ()) { + generation = 1; + } + else { + generation += 1; + } + njson_handle_generations[index] = generation; + njson_handle_store[index] = std::make_unique (std::move (value)); njson_ensure_keys_cache_size (njson_handle_store.size ()); return id; } njson_handle_store.push_back (std::make_unique (std::move (value))); + njson_handle_generations.push_back (1); njson_ensure_keys_cache_size (njson_handle_store.size ()); s7_int id = static_cast (njson_handle_store.size () - 1); return id; @@ -381,10 +428,19 @@ scheme_to_njson_scalar_or_handle (s7_scheme* sc, s7_pointer value, json& out, st out = static_cast (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) { @@ -795,6 +851,9 @@ njson_apply_update_on_root (s7_scheme* sc, json& root, const std::vectornjson "{\"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。 @@ -236,7 +265,7 @@ x : any 行为逻辑 -------- -1. 仅检查结构是否符合 njson 句柄格式(`(njson-handle . id)`)。 +1. 仅检查结构是否符合 njson 句柄格式(`(njson-handle . (id . generation))`)。 2. 不负责判断该句柄是否已释放;“已释放”通常在具体 API 调用时触发错误。 返回值 @@ -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))) @@ -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! @@ -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))) @@ -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 @@ -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"))) @@ -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"))) @@ -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}"))