The gap
src/workspace/mod.rs rewrites a command's key positions to prefix them with the workspace tag. It does this with a hand-rolled key walker — four hardcoded name lists (ALL_KEYS_COMMANDS, TWO_KEY_COMMANDS, STORE_NUMKEYS_COMMANDS, STORE_ALL_COMMANDS) plus a handful of if cmd.eq_ignore_ascii_case(...) special cases — and everything that matches none of them falls through to a single-key default:
// --- Default: single-key command — prefix args[0] only ---
let mut out = args.to_vec();
out[0] = prefix_frame(&args[0], ws_id);
out
For a command that writes a key the walker does not know about, that destination is written unprefixed — i.e. outside the workspace.
Not covered by any of the four lists today:
| Command |
Destination position |
GEOSEARCHSTORE |
args[0] (prefixed by accident — but args[1], the SOURCE, is not) |
ZRANGESTORE |
args[0] (same accident; source unprefixed) |
PFMERGE |
args[0] (same accident; sources unprefixed) |
SORT ... STORE dst |
keyword-positional, never prefixed |
GEORADIUS*/... STORE|STOREDIST dst |
keyword-positional, never prefixed |
BITOP dest src... |
not in any list |
COPY, LMOVE etc. |
in TWO_KEY_COMMANDS, fine |
GEORADIUS ... STORE joined this list with moon#645, which is what surfaced the gap — the file carried a comment claiming it handled that case:
// --- GEORADIUS/GEORADIUSBYMEMBER STORE variants: args[0] is key, STORE/STOREDIST keys ---
// For simplicity, prefix args[0] only (standard single-key behavior).
// The STORE dest key is a separate concern handled later if needed.
moon#645 replaced that comment with an accurate one, but did not fix the walker — appending one more name to a list that is already wrong for five other commands would make the drift worse, not better.
Why this is one bug, not six
This is the fifth independent key-extraction implementation in the tree (acl::keyspec::command_key_positions, tracking::invalidation::written_of, command::metadata's first_key/last_key, COMMAND GETKEYS, and this). moon#582 already consolidated the first two behind command_key_positions, and cross_shard_multikey_rejection (moon#592) and the ACL layer both call it — so the correct layouts for every command above already exist in the tree and are already tested.
Proposed fix
Replace the four lists with acl::keyspec::command_key_positions, prefixing exactly the positions it reports and leaving the rest of the argv alone. KeyPositions::AtPlusComputed (SORT ... BY w_*) needs a decision: the computed weight keys cannot be enumerated, so either they are left unprefixed (leaking reads across the workspace boundary) or SORT ... BY is refused inside a workspace.
Severity
Depends on whether workspaces are a shipped multi-tenancy guarantee or a dev-only feature. If the former this is a tenant-isolation break, not a cosmetic gap: a SORT k STORE dst inside workspace A writes a global dst that workspace B can read and overwrite.
Found while implementing moon#645; not introduced by it.
The gap
src/workspace/mod.rsrewrites a command's key positions to prefix them with the workspace tag. It does this with a hand-rolled key walker — four hardcoded name lists (ALL_KEYS_COMMANDS,TWO_KEY_COMMANDS,STORE_NUMKEYS_COMMANDS,STORE_ALL_COMMANDS) plus a handful ofif cmd.eq_ignore_ascii_case(...)special cases — and everything that matches none of them falls through to a single-key default:For a command that writes a key the walker does not know about, that destination is written unprefixed — i.e. outside the workspace.
Not covered by any of the four lists today:
GEOSEARCHSTOREargs[0](prefixed by accident — butargs[1], the SOURCE, is not)ZRANGESTOREargs[0](same accident; source unprefixed)PFMERGEargs[0](same accident; sources unprefixed)SORT ... STORE dstGEORADIUS*/... STORE|STOREDIST dstBITOP dest src...COPY,LMOVEetc.TWO_KEY_COMMANDS, fineGEORADIUS ... STOREjoined this list with moon#645, which is what surfaced the gap — the file carried a comment claiming it handled that case:moon#645 replaced that comment with an accurate one, but did not fix the walker — appending one more name to a list that is already wrong for five other commands would make the drift worse, not better.
Why this is one bug, not six
This is the fifth independent key-extraction implementation in the tree (
acl::keyspec::command_key_positions,tracking::invalidation::written_of,command::metadata'sfirst_key/last_key,COMMAND GETKEYS, and this). moon#582 already consolidated the first two behindcommand_key_positions, andcross_shard_multikey_rejection(moon#592) and the ACL layer both call it — so the correct layouts for every command above already exist in the tree and are already tested.Proposed fix
Replace the four lists with
acl::keyspec::command_key_positions, prefixing exactly the positions it reports and leaving the rest of the argv alone.KeyPositions::AtPlusComputed(SORT ... BY w_*) needs a decision: the computed weight keys cannot be enumerated, so either they are left unprefixed (leaking reads across the workspace boundary) orSORT ... BYis refused inside a workspace.Severity
Depends on whether workspaces are a shipped multi-tenancy guarantee or a dev-only feature. If the former this is a tenant-isolation break, not a cosmetic gap: a
SORT k STORE dstinside workspace A writes a globaldstthat workspace B can read and overwrite.Found while implementing moon#645; not introduced by it.