Skip to content

ks_json_check: decimal_between_zero_and_one mixed an exact floor with a truncating ceiling - #263

Open
mjerris wants to merge 1 commit into
masterfrom
fix/decimal-between-zero-and-one-mixed-bounds
Open

ks_json_check: decimal_between_zero_and_one mixed an exact floor with a truncating ceiling#263
mjerris wants to merge 1 commit into
masterfrom
fix/decimal-between-zero-and-one-mixed-bounds

Conversation

@mjerris

@mjerris mjerris commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

ks_json_check_number_is_decimal_between_zero_and_one reads valuedouble for its
lower bound but valueint for its upper. valueint is cJSON's saturating truncation
toward zero, so valueint <= 1 admits every value below 2 — the predicate enforces
(0, 2), not the [0, 1] its name promises.

/* src/ks_json_check.c:104, before */
return ks_json_type_is_number(item) && (item->valuedouble > 0.0f && item->valueint <= 1.0f);

The defect runs in two directions at once

value before after
-0.5 rejected rejected
0.0 REJECTED accepted the string_ twin has always accepted it
0.5 accepted accepted
1.0 accepted accepted
1.5 ACCEPTED rejected the [1,2) band the name excludes
1.9999 ACCEPTED rejected
2.0 rejected rejected so the over-accept band is exactly [1,2)
42 rejected rejected

Three values change; each is a correction. Everything else is untouched, so this
narrows to the documented domain rather than relaxing anything.

The two same-named variants disagreed with each other

ks_json_check_string_is_decimal_between_zero_and_one (src/ks_json_check.c:82) was
already correct — atof on both bounds, inclusive lower:

return ks_json_type_is_string(item) && ks_is_number(item->valuestring)
    && (atof(item->valuestring) >= 0.0f && atof(item->valuestring) <= 1.0f);

So a caller's accepted set depended on whether the value arrived number- or
string-encoded, for two functions sharing a name and a documented contract. This change
makes the number_ arm agree with the string_ arm exactly.

The inclusive lower bound is taken from the twin rather than from the name alone:
both variants landed in the same commit, and the author's contemporaneous reading of
"between zero and one" includes zero.

Impact

No callers exist in this repo — 2 definitions, 2 declarations, 0 call sites — so nothing
here changes behaviour today. The exposure is a downstream consumer calling a public API
whose name reads as obviously safe for a 0..1 parameter and silently getting (0, 2)
with 0.0 refused.

Provenance

Found while auditing a class of validator-truncation defects in the SignalWire spec
pipeline, where predicates read valueint and therefore compare a truncated value
against a bound spelled for a real number. The identical defect exists in
mod_infrastructure's independently-written parallel copy (cjson_check.c:98) and has
been fixed there in the same way; the two copies had drifted identically. This PR is the
other half — the defect survives in any consumer linking libks until both land.

This one is unusual within that class: the others truncate consistently, so their
enforced bound is merely shifted. This one is inconsistent within a single &&, and
inconsistent with its own correct twin.

… a truncating ceiling

`ks_json_check_number_is_decimal_between_zero_and_one` read `valuedouble` for
its lower bound but `valueint` for its upper. `valueint` is cJSON's saturating
truncation toward zero, so `valueint <= 1` admits every value below 2, and the
predicate enforced (0,2) rather than the [0,1] its name promises.

The defect ran in two directions at once:

  0.0     REJECTED  -- though the string_ twin one function above accepts it
  1.5     ACCEPTED  -- the whole [1,2) band the name excludes
  1.9999  ACCEPTED
  2.0     rejected  -- so the over-accept band is exactly [1,2)

So a caller's accepted set depended on whether the value arrived number- or
string-encoded, for two functions with the same name and the same documented
contract.

`ks_json_check_string_is_decimal_between_zero_and_one` (:82) was already
correct, using atof on both bounds with an inclusive lower. This change makes
the number_ arm agree with it exactly: valuedouble on both bounds, inclusive
lower. The two variants now enforce the same domain.

The inclusive lower bound is taken from the string_ twin rather than from the
name alone: both variants landed together, and the author's contemporaneous
reading of "between zero and one" includes zero.

No callers exist in this repo (2 definitions, 2 declarations, 0 call sites),
so nothing here changes behaviour today. The exposure is a downstream consumer
calling a public API whose name reads as obviously safe for a 0..1 parameter
and silently getting (0,2) with 0.0 refused.

The identical defect was fixed in mod_infrastructure's parallel copy
(cjson_check.c:98), which is where it was found. The two copies were written
independently and drifted the same way; this restores them to agreement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant