Repository navigation
Description
Summary
RedisNetworkExecutor hands a database-prefixed key straight to the engine's global-variable map:
if (resolved.database() != null)
return resolved.database().setGlobalVariable(resolved.key(), value);and every one of LocalDatabase.getGlobalVariable, setGlobalVariable, setGlobalVariableIfAbsent,
setGlobalVariableIfPresent, computeGlobalVariable opens with:
if (name.startsWith("$"))
name = name.substring(1);That strip exists for SQL, where $name and name are two spellings of one variable and the $ is syntax.
A Redis key is not syntax, it is opaque client data, and $ is a perfectly ordinary byte in it. So the
Redis wire has a key space in which k and $k are the same key, "" and $ are the same key, and
nothing tells the client.
The broken rule: a wire protocol's key space is the client's, and the server may not rewrite a name in
it. The place the rule is stated is SQLQueryEngine.validateVariableName, which the same five methods
call right after the strip - i.e. the map is explicitly a SQL variable namespace, and the Redis wrapper
stores arbitrary user keys in it anyway.
Consequences, in order of how badly they fail:
- one key silently overwrites another (
SET <db>.sess AthenSET <db>.$sess Bleaves one value); GETDEL <db>.$kdeletes<db>.k;INCR <db>.$ctrincrements<db>.ctr;SET <db>. vandSET <db>.$ vare one key, so a trailing-dot key is reachable under two names.
None of them reports an error, which is what makes it worse than a refusal.
Where (HEAD 98f15fe)
redisw/src/main/java/com/arcadedb/redis/RedisNetworkExecutor.java-resolveKeyAndDatabase(),
getVariable(),setVariable(),setVariableIfAbsent(),setVariableIfPresent(),
computeVariable(),removeVariable(),containsVariable(): all pass the client's key through unchangedengine/src/main/java/com/arcadedb/database/LocalDatabase.java- thename.startsWith("$")strip in
getGlobalVariable,setGlobalVariable,setGlobalVariableIfAbsent,setGlobalVariableIfPresent,
computeGlobalVariable
Repro
A server with the Redis plugin and a database catalogdb, one redis-cli per command:
SET catalogdb.sess user-A -> OK
SET catalogdb.$sess user-B -> OK
GET catalogdb.sess -> user-B <- the other client's write
GET catalogdb.$sess -> user-B
SET catalogdb.ctr 5 -> OK
INCR catalogdb.$ctr -> 6
GET catalogdb.ctr -> 6 <- a different key was incremented
SET catalogdb.empty E -> OK
SET catalogdb. DOT -> OK
GET catalogdb. -> DOT
GET catalogdb.$ -> DOT <- "" and "$" are one key
GETDEL catalogdb.$k1 -> two
GET catalogdb.k1 -> (nil) <- deleted through the other name
Real Redis keeps sess and $sess apart, and GETDEL $k1 leaves k1 alone.
Suggested fix
Do not store a client's key in the SQL variable namespace. The narrow fix is for the Redis wrapper to hold
its own per-database ConcurrentHashMap<String, Object> - the same structure defaultBucket already is for
the unprefixed keys - so a Redis key is never rewritten and the two halves of the key space behave alike.
If the global-variable map has to stay the storage, the wrapper must escape the key on the way in and
unescape it on the way out, in ONE place that both the read and the write path call.
The guarantee that stops it coming back: a round-trip property test over the Redis key space - for a
set of keys including $k, k, "", $, $$k, a.b, non-ASCII and the maximum length, assert that
writing a distinct value to each and reading them all back yields exactly what was written, for the
database-prefixed form AND the connection-local form. That one test also pins the two halves of the key
space against each other, which is the second half of the defect here.
Scope / confidence
Reproduced end to end against a running server with redis-cli. Refuted the obvious alternatives: it is not
redis-cli quoting (the shell passes $sess single-quoted, and the server's own reply shows the aliased
value), and it is not the connection-local bucket (the unprefixed form keeps the two names apart, which is
how I know the rewriting is the database path's). I also checked that nothing in the Redis wrapper itself
rewrites the key: it passes resolved.key() through verbatim, so the strip is the engine's.
Not new: the strip predates the current Redis work. That performance work moved
GET/SET/GETDEL/EXISTS/INCR*/DECR* into UNGATED_COMMANDS on the grounds that they touch only
"keys held in memory", which is accurate - I checked setGlobalVariable and it is a pure in-memory map, so
that change is clean.
Activity
ruispereira commentedon Oct 26, 2026
Correction: our refutation section in this issue overstates, and the fix is wider than we implied.
We wrote that nothing in the Redis wrapper itself rewrites the key. That is true of
RedisNetworkExecutorand false of the module.RedisQueryEngine.normalizeRamKeyandnormalizeRamReadKeystrip the same$deliberately, with a javadoc citing the review of PR #8309.Two consequences:
The aliasing reaches a second surface, so a fix applied only to the wire executor leaves the
redisquery language answering the old way, and the two surfaces of the same database then disagree about which key a name refers to.The module does have a place where this decision is already written down, which is better news than our text suggested: whatever rule you settle on has one existing home rather than needing a new one.
Re-validated at main 888b83c, still reproducing on both halves:
fix(#10694): a Redis key starting with $ is no longer the same key as…
Merge origin/main: #10694 is fixed by the raw global-variable store, …
fix(#10790, #10752, #10707, #10567): a Redis key is the client's key,…
robfrank commentedon Oct 28, 2026
Fixed in #10791. On the database-backed Redis key space,
<db>.$kand<db>.kwere the same key because the SQL variable map strips one leading$. Keys are now escaped in one place so the map is keyed by the client's key verbatim, and theredisquery language uses the same rule. Keys still remain SQL variables (SET <db>.kis$kin SQL).Still open: #10790 (keys named
parent,current,depth,path,stack,historyare refused as reserved SQL variables; needs a design decision).❤️ Thanks for helping make ArcadeDB better! ArcadeDB is free and open source, sustained by its community. If it's useful to you or your company, please consider becoming a sponsor to keep development going.
fix(#10694): a Redis key starting with $ is no longer the same key as…
fix(#10790, #10752, #10707, #10567): a Redis key is the client's key,…