Skip to content

query_graph: a non-numeric LIMIT/SKIP operand (e.g. LIMIT $limit) is silently dropped — the query runs unbounded and reports success #1994

Closed

Description

@metehanulusoy

What happens

parse_return_or_with() accepts SKIP/LIMIT only when the next token is a number. When it is not, the clause is discarded and parsing continues as if it were never written. The query then executes with no limit and returns isError: false.

Reproduced on main (3627eff), built from source, against a 26-node fixture repo:

$ cbm cli --json query_graph --project demo --query 'MATCH (n) RETURN n.name LIMIT 1'
rows: 1

$ cbm cli --json query_graph --project demo --query 'MATCH (n) RETURN n.name LIMIT $limit'
rows: 26          # LIMIT silently ignored, isError=false

$ cbm cli --json query_graph --project demo --query 'MATCH (n) RETURN n.name SKIP $offset LIMIT 1'
rows: 26          # the orphaned operand also swallows the LIMIT that follows

$ cbm cli --json query_graph --project demo --query 'MATCH (n) RETURN n.name LIMIT abc'
rows: 26

Expected: a parse error naming the offending token. Actual: a successful response containing the entire result set.

$param is not an exotic input — it is what a Neo4j-shaped client or an LLM agent writes by default. On a real repository the same query returns every matching row up to the 100k ceiling, or dies with result exceeded 100k rows — use narrower filters or add LIMIT, which tells the user to add a LIMIT they did write.

Root cause

src/cypher/cypher.c:1821-1834:

    /* Optional SKIP */
    if (match(p, TOK_SKIP)) {
        const cbm_token_t *num = expect(p, TOK_NUMBER);
        if (num) {
            r->skip = (int)strtol(num->text, NULL, CBM_DECIMAL_BASE);
        }
    }

    /* Optional LIMIT */
    if (match(p, TOK_LIMIT)) {
        const cbm_token_t *num = expect(p, TOK_NUMBER);
        if (num) {
            r->limit = (int)strtol(num->text, NULL, CBM_DECIMAL_BASE);
        }
    }

expect() (cypher.c:492-500) fills p->error and returns NULL, but both call sites only test if (num) and fall through to *out = r; return 0;. r->limit keeps the -1 initialiser set at cypher.c:1770, which cypher.c:4823 reads as "no LIMIT":

    rb_apply_skip_limit(rb, ret->skip, ret->limit >= 0 ? ret->limit : max_rows);

Two further layers hide the failure:

  1. cbm_lex() skips unknown characters (cypher.c:410-411, /* Unknown character — skip */ i++;), so $ disappears and the operand lexes as an ordinary token.
  2. parse_post_where() does not require TOK_EOF after the RETURN clause, so the orphaned token is dropped as well — which is why SKIP $offset LIMIT 1 loses both clauses.

Because every sub-parse returned 0, cbm_parse() never consults p.error and the caller gets error == NULL.

Why this is the failure mode #1334 already banned

tests/test_cypher.c:410-411 says of the ORDER BY cap fix:

more keys than the modeled maximum is a loud parse error - the old failure mode (ignore the remainder, drop the LIMIT) must never come back.

Same failure mode, different route: #1334 closed the ORDER BY key-list path; the SKIP/LIMIT operand path was left open.

Fix

Propagate the expect() failure in both branches — free_return_clause(r); return CBM_NOT_FOUND; — matching the ORDER BY branch immediately above. cbm_parse() then surfaces the existing expected token type ... got ... at pos N message.

PR follows with four regression tests: three fail on main and pass with the fix, and a control asserting SKIP 2 LIMIT 10 still parses and still carries 2/10. scripts/test.sh is green locally (clean ASan+UBSan build, all suites, contract steps).

Separately, the (int)strtol(..., NULL, ...) truncation at the same lines means a literal ≥ 2^31 also lands on a wrong value, but that needs an absurd literal and is not worth its own issue; the operand check above is the reachable defect.

Environment

  • main @ 3627eff, built from source with scripts/build.sh
  • macOS 15 (Darwin 25.6.0), arm64, clang

Activity

  1. added
    cypherCypher query language parser/executor bugs
    parsing/qualityGraph extraction bugs, false positives, missing edges
    on Sep 1, 2026
  2. added a commit that references this issue on Sep 2, 2026
    3d9a6cf
  3. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Sep 3, 2026
  4. DeusData commented on Sep 3, 2026

    @DeusData
    Owner

    Thank you for the precise bounded-versus-unbounded comparison.

    The source diagnosis is confirmed on current main: both SKIP and LIMIT call expect(TOK_NUMBER) but fall through after a failed expectation, leaving the no-limit sentinel in place. PR #2002 contains the focused propagation fix and reproduce-first coverage, and is already under maintainer review.

    This issue will remain open until that fix lands. Thank you for flagging the visible behavior change explicitly rather than hiding it as parser cleanup.

  5. DeusData commented on Sep 20, 2026

    @DeusData
    Owner

    Fixed on main in d6082f2 (PR #2002, by @metehanulusoy).

    A non-numeric SKIP/LIMIT operand no longer drops the clause silently. Previously expect() returned NULL, the clause was discarded, r->limit stayed at its -1 "no LIMIT" sentinel, and the query still reported success — so a bounded query answered with the entire result set. It is now a loud parse error (CBM_NOT_FOUND, clause freed), the same contract #1334 established.

    Worth noting for anyone searching this later: the everyday trigger is $limit, a Cypher parameter, which is what a Neo4j-shaped client emits by default.

  6. added 2 commits that reference this issue on Sep 29, 2026
    c2e67ff
    f8189c1
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

    bugSomething isn't workingcypherCypher query language parser/executor bugsparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions