From 93d1e63554ca79e0801b9b8a5a19c309e9ee6716 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 1 Nov 2026 13:14:46 +0100 Subject: [PATCH] fix(node): count only values against the node-answer value cap decodeCappedValues is documented to count JSON values, but it also counted object keys and closing brackets, so a node's GUID-keyed online and IP trees reached the cap at roughly half the stated number of values, and the master then refused the whole report rather than the excess. Keys and closing brackets no longer count; every value, nested ones included, still does. --- internal/web/runtime/remote_decode.go | 24 ++++++++++++++++++++---- internal/web/runtime/remote_test.go | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/internal/web/runtime/remote_decode.go b/internal/web/runtime/remote_decode.go index ec4c92c78..d4d9e599a 100644 --- a/internal/web/runtime/remote_decode.go +++ b/internal/web/runtime/remote_decode.go @@ -23,20 +23,36 @@ const ( var errRemoteListTooLong = json_util.ErrListTooLong // decodeCappedValues unmarshals raw into v after checking it holds at most maxValues -// JSON values in all, nested ones included. +// JSON values in all, nested ones included; object keys and closing brackets are not values. func decodeCappedValues(raw json.RawMessage, v any, maxValues int) error { dec := json.NewDecoder(bytes.NewReader(raw)) - for count := 0; ; count++ { + // One frame per open container; an object's strings alternate between key and value. + type frame struct{ object, wantKey bool } + var open []frame + count := 0 + for { tok, err := dec.Token() if err != nil { break } - if delim, ok := tok.(json.Delim); ok && (delim == ']' || delim == '}') { + delim, isDelim := tok.(json.Delim) + if isDelim && (delim == ']' || delim == '}') { + open = open[:len(open)-1] continue } - if count > maxValues { + if top := len(open) - 1; top >= 0 && open[top].object { + if open[top].wantKey { + open[top].wantKey = false + continue + } + open[top].wantKey = true + } + if count++; count > maxValues { return fmt.Errorf("%w (%d values)", errRemoteListTooLong, maxValues) } + if isDelim { + open = append(open, frame{object: delim == '{', wantKey: delim == '{'}) + } } return json.Unmarshal(raw, v) } diff --git a/internal/web/runtime/remote_test.go b/internal/web/runtime/remote_test.go index 1720514a1..5fee05759 100644 --- a/internal/web/runtime/remote_test.go +++ b/internal/web/runtime/remote_test.go @@ -554,3 +554,17 @@ func TestRemoteForgetInboundDropsEveryTrace(t *testing.T) { t.Fatalf("left behind: pushedFP=%v ids=%v aliases=%v", r.pushedFP, r.remoteIDByTag, r.adoptedAliases) } } + +// The cap is documented in JSON values; counting keys and closing brackets as well refused a +// node's whole online report at well under the stated number of values. +func TestDecodeCappedValuesCountsValuesOnly(t *testing.T) { + var got map[string][]string + // One object, two arrays and three strings: six values, though fourteen tokens. + raw := json.RawMessage(`{"guid-a":["x","y"],"guid-b":["z"]}`) + if err := decodeCappedValues(raw, &got, 6); err != nil { + t.Fatalf("six values under a cap of six: %v", err) + } + if err := decodeCappedValues(raw, &got, 5); !errors.Is(err, errRemoteListTooLong) { + t.Fatalf("six values under a cap of five: err = %v, want errRemoteListTooLong", err) + } +}