Check three Valkey 9.1.3 results before the client upgrade

John Burns

Check three Valkey 9.1.3 results before the client upgrade

Valkey 9.1.3 and 9.1.2 split on three ordinary commands. A hash key named fields could not be written with HSETEX on 9.1.2. The same command wrote the field on 9.1.3. ZADD rejected the score 0x10 on 9.1.2 and stored 16 on 9.1.3. LSET with the index 4294967296 returned success on 9.1.2 and replaced the first list element. On 9.1.3 that index returned an error and the list stayed as it was.

Those three results are the check a client job has to make before it is pointed at 9.1.3. The 26 October 2026 notes call the release a security upgrade. The security note is about cluster publish messages. It does not mention a hash key, a hexadecimal score, or a list index. A job that only reads the advisory will miss the command changes the new binary actually makes.

What the two binaries were

The comparison used two servers built from the GitHub tag archives on Debian 13, Linux amd64, with gcc 14.2.0. Each tag commit matched the commit named on the release page:

9.1.2  7f1dffedff6de73058b2c2a389422b6ecd56c8fb
9.1.3  29173b67508b2de4c0f0ed551f3b47cf19fc3ca8

The SHA-256 of each GitHub tag archive, as downloaded for this build, was:

9.1.2  19c23908e7d57e8d91ef85b41f5646307582f10f4f0fb999bbf89ed24ec9c983
9.1.3  184e34ed2d9b51879b682b5b15205e2b9d677c60280e3cb453be8f520d04e293

Those digests are hashes of the archives used here. The project release page did not publish a separate checksum file. The tag commits are the source identity. The unpacked version header said 9.1.2 in one tree and 9.1.3 in the other.

Both trees were built with TLS disabled and the libc allocator. The running binaries printed:

Valkey server v=9.1.2 sha=00000000:1 malloc=libc bits=64
Valkey server v=9.1.3 sha=00000000:1 malloc=libc bits=64

The sha field is what a release tarball prints. It is not a git short hash. These builds have no TLS. The three commands do not go through the TLS path, so the missing TLS library does not explain the split. Both servers were standalone. Cluster mode was off. The cluster publish defect was not exercised, and these processes were not a cluster.

INFO server also printed redis_version 7.2.4 on both binaries, next to valkey_version 9.1.2 or 9.1.3. A probe that reads the Redis compatibility field will see the same string on both sides of this release. The Valkey field is the one that moved.

The hash key the parser ate

HSET on a key named fields worked on 9.1.2. It returned 1, and HGET returned the value. HEXPIRE on that same key, with a 10 second TTL and one field, also worked on 9.1.2 and left a TTL of 10. The key name is legal. The failure is specific to HSETEX and HGETEX.

On 9.1.2 these commands all returned the same error and wrote nothing:

HSETEX fields FIELDS 1 f v
HSETEX Fields FIELDS 1 f v
HSETEX FIELDS FIELDS 1 color blue
HSETEX fields EX 10 FIELDS 1 ttl v2
HGETEX fields FIELDS 1 f
(error) ERR value is not an integer or out of range

HGET after the failed HSETEX was nil. The case of the key does not matter. fields, Fields, and FIELDS all failed. A client that changes the capitalization to dodge a reserved word still hits the error. The token match is on the argument value.

The same HSETEX fields FIELDS 1 f v on 9.1.3 returned integer 1. HGET returned the value. HGETEX fields FIELDS 1 f returned the value as a one-element array, not as a bulk string. That array shape is the HGETEX reply, not a side effect of the fix. The expiration form also landed. HSETEX fields EX 10 FIELDS 1 ttl v2 returned 1, and HTTL for that field returned 10.

A control key named myhash succeeded on both binaries. The command is not broken in general on 9.1.2. It is broken when the key is the word the parser is scanning for, including when that word appears before the real FIELDS token. The 19 October fix starts that scan after the command name and the key. The release note says the commands fail to parse when the key is named fields. It does not say that HSET and HEXPIRE already accept that key on the broken binary, and it does not say that Fields and FIELDS fail the same way. Both showed up in this run.

HSETEX fields FIELDS 0 on 9.1.3 is still an error: wrong number of arguments for hsetex. The fix does not make a short command succeed. A job that treats any non-error as proof of the upgrade has to send a complete command and then read the field back.

The score 9.1.2 refused

ZADD with the decimal score 16 succeeded on both binaries. ZADD with the score 0x10 did not.

On 9.1.2 the reply was:

(error) ERR value is not a valid float

ZSCORE was nil. The uppercase form 0X10 failed the same way. On 9.1.3 both forms returned integer 1, and ZSCORE was the string 16. The stored score is decimal 16, not the text 0x10. A later read that expects the original token will not see it. A later read that expects 16 will.

The fractional token 0x1.8p1 split the same way. 9.1.2 rejected it as not a valid float. 9.1.3 stored 3. That is 1.5 times two, which is what a C hex-float parser does with that token. The release note gives 0x10 as the example. It does not give the fractional form. A replica or an AOF line that contains only 0x10 is not the only token this change accepts.

The 9.1.3 notes say the change restores acceptance so a 9.0 primary can replicate a hex score to 9.1. This comparison did not run 9.0. It did not replay an AOF. It recorded what the two 9.1 binaries do with the token when a client sends it.

The index that rewrote element zero

A two-element list, a then b, is enough to see the LSET change. LSET with index 2 is a normal out-of-range index on that list. Both binaries returned an index out of range error and left the list alone. A job that only tests an index one past the end will not see the defect.

LSET with index 4294967296 is the index that splits. On 9.1.2 the reply was OK. The list became z then b. The first element changed. The second did not. On 9.1.3 the reply was an index out of range error and the list stayed a then b.

The negative form does the same rewrite on the old binary. After the list was z then b, LSET with index -4294967296 returned OK on 9.1.2 and the list became neg then b. It did not modify the last element. It modified the first one again. On 9.1.3 the same negative index returned the range error and left the list unchanged.

The 3 October description of the fix says a 64-bit build narrowed the index to a 32-bit integer, so 4294967296 became 0. That matches what this binary did: both the large positive index and the large negative index replaced element 0, and a merely-too-large index of 2 did not. This host is 64-bit. The notes say a 32-bit build rejects these values while parsing the index. That path was not built here.

An upgrade check that looks only for a nonzero LSET status will miss the old behavior. 9.1.2 returned success. The evidence is the list contents, not the status line. Read the list after the command. If element 0 changed and the index was 4294967296, the binary is the old one.

The security note is a different check

The 9.1.3 and 9.0.7 notes, both dated 26 October 2026, mark the upgrade urgency as security. Advisory GHSA-f4q9-j22m-xqq9 describes an integer truncation while a cluster-mode server builds a publish message. An authenticated client on a cluster-mode server can crash the process. The project says the overflowing bytes are chosen by the client, so code execution as the server user may be possible. Servers that are not in cluster mode are not affected. The patched versions named in the advisory are 9.2.0-rc2, 9.1.3, 9.0.7, 8.1.11, 8.0.12, and 7.2.15. Affected versions include 9.1.2, 9.0.6, 8.1.10, 8.0.11, and 7.2.14.

This comparison did not build a cluster and did not attempt the defect. Short of the upgrade, the vendor mitigation is to restrict scripting commands when the application does not use them, and not to raise the maximum bulk length above its default. That mitigation is not a substitute for the upgrade on a cluster-mode server. It also does not change the three command results above. A standalone server still has the HSETEX, ZADD, and LSET behavior of whatever binary it is running.

The 9.0.7 notes list the same HSETEX and LSET fixes. They do not list the hex-float restoration. That note is on the 9.1.3 page, where 9.1 had dropped the 9.0 behavior. This run did not start a 9.0.6 or 9.0.7 server. Do not treat the 9.1.2 versus 9.1.3 results as a measurement of the 9.0 line.

The Debian package is not 9.1.3

Debian’s 27 October 2026 advisory DSA-6586-1 fixes valkey in trixie at 8.1.1+dfsg1-3+deb13u3. It names seven CVE identifiers. It does not name GHSA-f4q9-j22m-xqq9, and the upstream advisory lists no CVE. The upstream fix for the 8.1 line is 8.1.11. A Debian revision of 8.1.1 is not that version string. Installing the DSA package and then expecting HSETEX is the wrong check. The command page says HSETEX exists since 9.0.0. This comparison did not install the Debian package and did not run 8.1.

The published trixie-security package index, fetched during this run, listed valkey-server 8.1.1+dfsg1-3+deb13u3. The trixie main index still listed 8.1.1+dfsg1-3+deb13u2. The local apt-cache policy on this host, which had not been refreshed, still showed 8.1.1+dfsg1-3+deb13u2 from both trixie and trixie-security. A decision that reads the stale local policy will miss the advisory version that the security index already published. Refresh the index, then compare the candidate with 8.1.1+dfsg1-3+deb13u3. That comparison answers the Debian advisory. It does not answer the 9.1.3 command results.

Verification before the client moves

Print the version from the binary the client will talk to.

valkey-server --version

Expect v=9.1.3 for the upstream line tested here, or the patched version the advisory names for the line you actually run. Ignore redis_version in INFO. On both of these binaries it was 7.2.4.

Use an empty database. The commands below write keys named fields, zs, and lst.

valkey-cli HSET fields f old
valkey-cli HSETEX fields FIELDS 1 f v
valkey-cli HGET fields f
valkey-cli ZADD zs 0x10 member
valkey-cli ZSCORE zs member
valkey-cli RPUSH lst a b
valkey-cli LSET lst 4294967296 z
valkey-cli LRANGE lst 0 -1

On 9.1.3, HSET and HSETEX both succeed, HGET returns v, ZADD succeeds, ZSCORE returns 16, LSET returns an index error, and LRANGE is still a then b. On 9.1.2, HSET succeeds, HSETEX returns the integer-or-range error, HGET is still old, ZADD returns the invalid-float error, and LSET returns OK with the list now z then b.

Also send LSET with index 2 on that two-element list. Both versions return the range error. If that is the only index you try, the old binary looks fixed.

If the server is in cluster mode, the upgrade reason is the advisory, not these three commands. If it is standalone, these three commands are still the client-visible change between 9.1.2 and 9.1.3. Either way, read the version from the binary, then read the key, the score, and the list. The release note does not do that for you.

Sources