Skip to content

Escape tabs and newlines in keys for the WAL/SSTable line format#114

Open
mweiden wants to merge 1 commit into
mainfrom
claude/audit-fix-2-key-escaping
Open

Escape tabs and newlines in keys for the WAL/SSTable line format#114
mweiden wants to merge 1 commit into
mainfrom
claude/audit-fix-2-key-escaping

Conversation

@mweiden

@mweiden mweiden commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Problem

WAL and SSTable records are stored as key\tbase64(value)\n. Values are base64-encoded and safe, but keys were written raw β€” and keys are built from user-supplied partition/clustering values. Inserting a value like 'a\tb' (or anything containing a newline) split the record at the wrong byte on WAL replay and on every SSTable scan, silently corrupting that record and the parse of everything after it.

Fix

  • New util::escape_key / util::unescape_key (backslash escaping: \t, \n, \\), Cow-based so plain keys don't allocate.
  • WAL records escape the key on write (lib.rs::insert_internal) and unescape on replay (wal.rs::parse_entries).
  • SSTables sort, index, and probe by the escaped form so binary search and sorted early-exit scans stay consistent; bloom filters and zone maps keep operating on raw keys. scan_ns unescapes parsed keys.

Compatibility: keys without special characters are byte-identical to the old format, and unknown escape sequences are preserved verbatim on decode, so existing data files remain readable.

Tests

  • wal_recovery_preserves_keys_with_delimiters: hostile keys (tab/newline/backslash mixes) survive restart, neighbouring records intact, no phantom rows.
  • sstable_roundtrip_preserves_keys_with_delimiters: lookups work after create and after a meta-less rebuild.
  • flush_and_query_preserve_partition_keys_with_delimiters: end-to-end through the SQL engine with flush.
  • Unit tests for escape/unescape round-trips and legacy-sequence tolerance.

Found by codebase audit (high severity: silent data corruption from ordinary inputs).

https://claude.ai/code/session_01CJXWjB9ERGgV6B9mRqrdM9


Generated by Claude Code

Records are stored as 'key\tbase64(value)\n'. Values are base64-encoded
and safe, but keys were written raw, and keys are built from
user-supplied partition/clustering values. A value like 'a\tb' or a
string containing a newline split the record at the wrong byte on WAL
replay and SSTable scans, silently corrupting that record and shifting
parsing for the rest of the file.

Keys are now backslash-escaped on disk (\t, \n, \\) and unescaped on
read. SSTables sort, index, and probe by the escaped form so binary
search and early-exit scans stay consistent; bloom filters and zone
maps keep operating on raw keys. Keys without special characters are
byte-identical to the previous format, so existing data files remain
readable.

https://claude.ai/code/session_01CJXWjB9ERGgV6B9mRqrdM9

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a889bb1d48

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/sstable.rs
}
// The file and index store escaped keys, so probe with the escaped
// form.
let escaped = crate::util::escape_key(key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep legacy SSTable backslash keys readable

When opening SSTables written before this commit, the on-disk key bytes and persisted sparse index are still raw. Escaping every lookup key here makes any pre-upgrade row whose key contains a backslash, such as user\1 or path\to, get probed as user\\1; the bloom/zone-map checks can pass, but seek_offset/scan_from compare against the raw legacy bytes and return None, making those rows unreadable after upgrade. Please detect/migrate legacy SSTables or fall back to raw-key probing when the escaped probe misses.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment thread src/wal.rs
.map_err(std::io::Error::other)?
.to_string();
.map_err(std::io::Error::other)?;
let key = crate::util::unescape_key(key).into_owned();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate WAL unescaping for legacy records

This unconditionally decodes keys from any existing WAL, but WAL records produced before this change stored backslashes literally. If an upgrade happens with an unflushed record whose key is a\t, a\n, or a\\b, replay inserts it under a different key (a<TAB>, a<NEWLINE>, or a\b), so reads by the original key fail or collide with another row. The WAL needs a version/marker or a legacy fallback before applying escape decoding to old records.

Useful? React with πŸ‘Β / πŸ‘Ž.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants