Skip to content

jq: a fold UPDATE/EXTRACT body nested under try/if has no register — del(foreach .a as $v (.; try ($v | .b); .)) silently discards the write #3145

Description

@newhoggy

Severity: Medium (del/|= through a fold's UPDATE silently discarded — exit 0, document echoed — where jq writes; the read forms emit nothing at exit 0)

Summary

#3133 (PR #3144) carries the path register into a pipe nested under try/if/, and into a catch handler for a pipe stage: resolve_seq_stage puts the carried register on the stage's Frame, and a nested resolve_seq_sink seeds itself from it. A fold's UPDATE/EXTRACT body takes a different route — FoldRegister::resolveresolve_seq with the fold's own register passed explicitly, under a frame that carries none — so the same shape inside a fold still starts register-less one level down: try ($v | .b) in UPDATE cannot re-establish $v, raises the resolver's own refusal, and try swallows it.

Found by #3133's widened fuzz alphabet (fabricate-baseline/mismatch-baseline rows, i.e. pre-existing on main).

Repro

/usr/bin/jq 1.7.1 vs succinctly (main 833aad9fd and the #3144 branch alike), input {"a":{"b":1}}:

filter jq 1.7.1 succinctly
path(foreach .a as $v (.; try ($v | .b); .)) ["a","b"] (nothing), exit 0
del(foreach .a as $v (.; try ($v | .b); .)) {"a":{}} {"a":{"b":1}}, exit 0 (write discarded)
path(foreach .a as $v (.; if true then ($v | .b) else . end; .)) ["a","b"] refuses (near attempt to access element "b" of {"b":1})
path(foreach .a as $v (.; ($v | .b); .)) (control, no nesting) ["a","b"] ["a","b"]
path(foreach .a as {b:$v} (.; try $v; .)) (control, no navigation in the try) ["a","b"] ["a","b"]

Fuzz-found originals (all reproduce on main):

path(foreach .x as {a:$v0, d:$w0} ?// {c:$v0} (.; try ($v0 | .b?); (try error(1) catch $v0)))
(foreach .x as {x:{c:$v0}} (0; try ($v0 | .b?); ($v0 | .b?))) = 9
(foreach .a as {a:$v0, d:$w0} ?// {c:$v0} (.; ($v0 | .b?)?; ($v0 | reduce (1) as $i (.; getpath(["a"]))))) |= .

Root cause

src/jq/eval.rs: FoldRegister::resolve hands its register to resolve_seq as the explicit register argument, but the Frame it resolves under (fold_frame.extend(&branch.path) / self.frame.unknown()) carries no register, so when UPDATE's stage is try (...)/if ... then (...), the Try/If arm's nested Pipe — which #3133 made seed from frame.register() — finds none.

Why this is separate from #3133

#3133 is the pipe-stage frame; this is the fold register's own frame, built in FoldRegister::enter/advance/relocate, which #3133 deliberately left alone (folds have their own register model, #1440/#1466/#2031). Same mechanism, different owner.

Suggested fix direction

Where FoldRegister::resolve builds the frame it resolves UPDATE/EXTRACT under, attach the register it is about to pass (frame.with_register(Some(&reg.value)) when the branch is untracked), so the nested-pipe seeding #3133 added applies. The step_may_reestablish trust in resolve_seq_stage already keys on the frame's register, so no second change is needed there. Add the rows above to scripts/jq-bind-origin-oracle-sweep.sh and rerun scripts/jq-bind-origin-fuzz.py (whose USES now draw nested-try bodies inside folds) at zero fabricate.

Refs #3133, #3144, #2979, #2676, #1440.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    OpusSuitable for an Opus-class model to implementbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions