Skip to content

[BUG] ZRANGEBYSCORE accepts bare "(" as valid score, silently treating it as exclusive 0.0 #3483

@madolson

Description

@madolson

Describe the bug

ZRANGEBYSCORE (and ZCOUNT, ZRANGESTORE, etc.) accepts a bare ( character as a valid score argument, silently parsing it as (0.0 (exclusive zero). Per the documentation, ( is a prefix that must be followed by a score value (e.g., (1.5 means exclusive 1.5). A bare ( with no number should return an error.

This causes incorrect query results without any indication to the user that their input was malformed.

Additionally, an empty string "" is also silently accepted as a valid score (parsed as 0.0).

Root cause

In src/t_zset.c, function zslParseRange (line ~636):

if (s[0] == '(') {
    spec->min = valkey_strtod_n(s + 1, len - 1, &eptr);
    if (eptr[0] != '\0' || isnan(spec->min)) return C_ERR;
    spec->minex = 1;
}

When s = "(" (length 1), valkey_strtod_n(s + 1, 0, &eptr) is called with zero length. The parser returns 0.0 and sets eptr to s + 1, which points to the null terminator. The check eptr[0] != '\0' passes, so ( is accepted as (0.0.

Coverity

This was flagged as CID 901320 (Out-of-bounds access, High impact) in the Coverity scan.

To reproduce

127.0.0.1:6379> ZADD zs -2 neg2 -1 neg1 0 zero 1 pos1 2 pos2
(integer) 5
127.0.0.1:6379> ZRANGEBYSCORE zs ( +inf
1) "pos1"
2) "pos2"
127.0.0.1:6379> ZRANGEBYSCORE zs -inf (
1) "neg2"
2) "neg1"
127.0.0.1:6379> ZCOUNT zs ( +inf
(integer) 2

The bare ( is silently treated as (0.0:

127.0.0.1:6379> ZRANGEBYSCORE zs (0 +inf
1) "pos1"
2) "pos2"

Both produce identical results, proving ( == (0.

Expected behavior

A bare ( should return an error like:

(error) ERR min or max is not a float

This is consistent with how other invalid score strings are handled (e.g., ZRANGEBYSCORE zs abc +inf returns an error).

Suggested fix

In zslParseRange, add a length check after detecting the ( prefix:

if (s[0] == '(') {
    if (len <= 1) return C_ERR;  // bare "(" with no number
    spec->min = valkey_strtod_n(s + 1, len - 1, &eptr);
    ...
}

Same fix needed for the max parsing branch.

Additional information

  • Valkey version: unstable (HEAD)
  • Also affects ZCOUNT, ZRANGESTORE, ZREVRANGEBYSCORE, and any command using zslParseRange
  • Found via Coverity static analysis + manual code audit
  • The documentation at valkey.io/commands/zrangebyscore states the ( character is for "prefixing the score" — a bare ( with no score is not a valid specification

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions