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
Describe the bug
ZRANGEBYSCORE(andZCOUNT,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.5means 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 as0.0).Root cause
In
src/t_zset.c, functionzslParseRange(line ~636):When
s = "("(length 1),valkey_strtod_n(s + 1, 0, &eptr)is called with zero length. The parser returns 0.0 and setseptrtos + 1, which points to the null terminator. The checkeptr[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
The bare
(is silently treated as(0.0:Both produce identical results, proving
(==(0.Expected behavior
A bare
(should return an error like:This is consistent with how other invalid score strings are handled (e.g.,
ZRANGEBYSCORE zs abc +infreturns an error).Suggested fix
In
zslParseRange, add a length check after detecting the(prefix:Same fix needed for the
maxparsing branch.Additional information
ZCOUNT,ZRANGESTORE,ZREVRANGEBYSCORE, and any command usingzslParseRange(character is for "prefixing the score" — a bare(with no score is not a valid specification