Liking cljdoc? Tell your friends :D

Red Team 2 — Consolidated Report

meme-clj v2.0.0 | 2026-04-02

Methodology: 50+ hypotheses tested via live nREPL + 5 deep-dive code analysis agents (tokenizer, parser, printer/formatter, rewrite/lang, CLI/runtime). Non-overlapping with Red Team 2026-04-01 and Magenta Team 2026-04-02.

Test baseline: 918 JVM + 771 CLJS + 11 meme example tests — all passing.


Scoreboard

SeverityCountExamples
HIGH6TRS #() corruption, double syntax-quote, #::{} empty ns, metadata CCE, EDN code exec, TRS :: divergence
MEDIUM15Symbol/keyword splits, BOM, rewrite non-termination, surrogate chars, bare :, register! override, restore-bare-percent
LOW14Comments dropped, null bytes, Unicode control chars, number greedy scan, metadata flat rendering
INFO5clj->meme sugar loss, emit fallback, error message quality
Total40

HIGH Findings (6)

H1. TRS #() semantic corruption

#(+(% 1)) → TRS emits #((+ % 1)) — extra parens make it call the return value as a function. Runtime ClassCastException. Classic/rewrite correctly emit #(+ % 1).

Location: src/meme/trs.cljc, src/meme/lang/meme_trs.cljc

H2. Double syntax-quote x `` wrong expansion

Clojure eval of x : `'redteam2/x` (namespace-qualified, one level of quoting). Meme eval of x: 'x (unqualified, wrong structure).

The inner backtick doesn't produce a proper nested expansion. Breaks macro-writing-macros that rely on ~~ ``x patterns.

Location: src/meme/parse/expander.cljc

H3. #::{} produces malformed keywords

Tokenizer's read-symbol-str consumes : as a symbol char after #:. Result: ns-name="", output {:/a 1} instead of current-namespace-qualified keywords. Clojure #::{} correctly resolves to current namespace.

Location: src/meme/scan/tokenizer.cljc:338-342, src/meme/parse/reader.cljc:517-520

H4. ^:foo 42 — raw ClassCastException

Metadata type check passes (keyword is valid metadata), but vary-meta on a number throws unhandled ClassCastException: Long cannot be cast to IObj. Missing metadatable? guard on the target before calling vary-meta.

Location: src/meme/parse/reader.cljc:431

H5. EDN lang loading = arbitrary code execution

--lang file.edn calls requiring-resolve on symbols in the EDN (loads namespaces from classpath) and slurp+eval on :run string paths. No sandboxing, no path validation, no allowlist.

Location: src/meme/lang.cljc:47-97, src/meme/runtime/cli.clj:77-83

H6. TRS ::keyword semantics differ from classic

TRS preserves ::foo verbatim in output; classic expands for deferred resolution. When Clojure output is read in a different namespace, ::foo resolves to the wrong namespace. Not covered by lang agreement tests.

Location: src/meme/trs.cljc, test/meme/trs_test.cljc:98-120


MEDIUM Findings (15)

M1. foo/bar/baz parsed as two forms

Meme splits at first /, producing foo/bar + /baz. Clojure reads as one symbol foo/bar/baz. Silent mis-parse.

M2. :foo/bar/baz keyword split

Same pattern as M1 but for keywords. Silently splits at the second slash.

M3. foo/ trailing slash accepted

Meme accepts and emits foo/. Clojure rejects: "Invalid token: foo/". Produces invalid Clojure output.

M4. foo/1bar digit-starting name accepted

Meme accepts foo/1bar as a symbol. Clojure rejects — name part after / cannot start with a digit.

M5. #'foo(bar)(var (foo bar)) → invalid Clojure

Meme parses #'foo(bar) as var-quote of a call → (var (foo bar)). Clojure's var only accepts symbols, not lists. Output: Syntax error compiling var.

M6. #'^:foo bar silently drops metadata

Meme: #'^:foo bar(var bar) — metadata ^:foo is lost. Clojure preserves metadata on the var form.

M7. BOM marker corrupts Clojure output

Input file: UTF-8 BOM + f(x y). Output file hex: 28 EF BB BF 66 20 78 20 79 29(ﻯf x y). The BOM bytes appear inside the opening paren, producing invalid Clojure.

M8. Rewrite non-termination on size-increasing rules

Rule ?x → [?x ?x] causes infinite loop / timeout. The 100-iteration cycle detection is bypassed because each iteration produces a different (larger) form. Needs a size budget or output-size cap.

M9. anon-fn-depth volatile not decremented on error paths

anon-fn-depth incremented at line 541, decremented at line 557. Any error between corrupts the counter. Compare: sq-depth correctly uses try/finally.

Location: src/meme/parse/reader.cljc:537-567

M10. Surrogate char literals \uD800-\uDFFF accepted

resolve-char lacks the surrogate range check that already exists in parse-unicode-escape for strings. Meme accepts, Clojure throws UnsupportedOperationException.

Location: src/meme/parse/resolve.cljc:88-96

M11. Bare : lone colon accepted as keyword

Produces (keyword ""). Clojure rejects: "Invalid token: :".

M12. #:{:a 1} empty namespace accepted

Clojure: "Namespaced map must specify a namespace". Meme: silently produces {:/a 1}.

M13. s->m-rules guard drops non-symbol/non-keyword heads

Lists with nil, true, false, or number heads are not tagged as m-call. clj->meme-text emits (nil 1 2) S-expression syntax instead of valid meme nil(1 2).

Location: src/meme/rewrite/rules.cljc:12-19

M14. register! allows silently overriding built-in langs

(register! :meme-classic {...}) hijacks the default lang with no warning. Process-global state mutation. No name-collision check against built-ins.

Location: src/meme/lang.cljc:175-189

M15. restore-bare-percent doesn't recurse into maps/sets

normalize-bare-percent in forms.cljc recurses into maps and sets. restore-bare-percent in printer.cljc does not. Result: #(#{%}) roundtrips as #(#{%1}), violating syntactic transparency.

Location: src/meme/emit/printer.cljc:71-79


LOW Findings (14)

IDFinding
L1TRS unexpanded syntax-quote — emits `(f ~x) instead of expanded seq/concat/list
L2~x outside syntax-quote: classic errors, rewrite/trs accept — inconsistency
L3Comment silently dropped by formatter — def(x ;; important\n 42)def(x 42)
L4Comment-only file produces empty output in to-clj
L5Null byte consumed as whitespace — f(x\0y)(f x y), silent data alteration
L6Zero-width space (U+200B) absorbed into symbol names — invisible character attack vector
L7RTL override (U+202E) accepted in symbols — display-level attack
L8match-pattern returns nil for splice variable ?&args — silently fails to match
L9read-number greedy: 1N.5 is one token (error), Clojure reads two forms. Same for 1-2, 1+2
L10##foo accepts any symbol after ## — confusing "Invalid number" error vs Clojure's "Invalid token"
L11\r-only line endings: all tokens report line 1 — sadvance! only increments on \newline
L12Metadata maps always rendered flat — ^{:doc "...long..."} committed to string before layout engine
L13flat/format-forms drops trailing file comments — canon handles them, flat does not
L14match-seq with multiple splice variables is O(n^k) — k splice vars on n elements

INFO Findings (5)

IDFinding
I1clj->meme loses reader sugar (', @, #()) — Clojure reader limitation, not meme bug
I2Rewrite engine crash on non-seq input — raw IllegalArgumentException instead of structured error
I3emit fallback to pr-str for AST node records — no error, produces garbage output
I4format-error drops info for non-ExceptionInfo exceptions — user code eval errors show no source context
I5REPL default resolve-keyword uses clojure.core/read-string — latent escalation point

Fix Priority (Highest ROI)

  1. H1 + H6 — TRS cross-validation: add #(inc(%)), ::foo to lang agreement tests, fix TRS #() wrapping
  2. H4 — One-line fix: add metadatable? check before vary-meta in reader.cljc:431
  3. M9 — One-line fix: wrap anon-fn-depth in try/finally like sq-depth
  4. M1-M4, M11-M12 — Single tokenizer layer: validate symbol/keyword structure after scanning
  5. M7 — Strip BOM in file reader path
  6. M10 — Copy surrogate range check from parse-unicode-escape to resolve-char
  7. H3 — Special-case #:: in tokenizer dispatch (don't let read-symbol-str consume :)
  8. M15 — Add map? and set? cases to restore-bare-percent

Defenses Confirmed (What We Failed to Break)

DefenseHypotheses Refuted
Number validation (1.2.3, 0xGG, 1/0, 1.5N, 1/2/3)5
Formatter idempotency at all widths including width=12
Error messages with accurate source locations5
CLI: missing files, unknown commands, wrong extensions5
Eval semantics for all tested programs5
200+ level nesting without stack overflow2
Duplicate map key detection1
Odd-element-count map detection1
Reader conditional validation (wrong arity, odd elements)2
Stage contract validation5
Regex validation for invalid patterns1
Character literal roundtrip (named, unicode, octal)5
Lang agreement on standard forms4
All 918 JVM + 771 CLJS tests pass

50+ hypotheses tested via REPL. 5 deep-dive code analysis agents. 40 findings confirmed across 4 severity levels. 70+ hypotheses refuted.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close