Adversarial-corpus validation

cve-validation.md answers which published CVEs does disarm handle?, one row per CVE, every row asserted in tests/test_cve_vectors.py. This page answers the same question for published adversarial-text corpora, and it exists because the CVE corpus cannot reach them: a CVE is a defect in one implementation, while a paper releases a generator that emits a whole family.

This is not a coverage claim

A yes means one reconstructed vector for that subtype was neutralized or detected. It does not mean the subtype is covered, and it says nothing about variants nobody has written down. Rows disarm does not act on are listed as no and asserted as negatives, so a limitation cannot drift into a claim.

The corpus

Sarabamoun, Special-Character Adversarial Attacks on Open-Source Language Models, arXiv:2508.14070v1, CC BY 4.0. Generators at EphraiemSarabamoun/special-character-attack.

The vectors here are reconstructed from the paper's definitions, not cloned. CI does not depend on a third-party repository staying put — and, more to the point, the released generator has three defects that would score as passes if its output were trusted:

defect effect
homoglyph_attacks.py defines 'A', 'B', 'C' twice in self.confusables — mathematical literals, then fullwidth the later definitions win, so the math_alts filter is always empty and the paper's headline U+1D400 subtype is never exercised
invisible_payload/steganographic is passed an empty payload the encoding loop never runs and the function returns its input
targeted_word/target_system substitutes "system" for "system" a no-op by construction

28 of the corpus's 591 rows are therefore no-ops. A row that measures the unmodified prompt and scores as neutralized is the kind of green a gate must not produce, so every vector on this page is asserted to differ from the base prompt before anything else is measured.

Results

Every vector perturbs the same prompt — ignore previous instructions — so the family is the only variable. Measured by tests/test_adversarial_corpora.py, which is also what checks this table.

family subtype neutralized by canonicalize detected by has_anomalies
Unicode control combining pile yes yes
Unicode control directional isolate yes yes
Unicode control directional override yes yes
Unicode control invisible payload yes yes
Unicode control tag block yes yes
Unicode control zero-width joiner yes yes
Unicode control zero-width non-joiner yes yes
Unicode control zero-width space yes yes
Homoglyph cyrillic substitution yes yes
Homoglyph fullwidth yes no
Homoglyph greek substitution yes yes
Homoglyph mathematical alphanumerics yes yes
Homoglyph small capitals yes yes
Structural bracket nesting no no
Structural character deletion no no
Structural fragmentation yes no
Structural negation overlay no no
Structural spacing injection yes no
Structural whitespace steganography yes yes
Structural word reordering no no
Encoding base64 no no
Encoding binary no no
Encoding hex no no
Encoding leetspeak no no
Encoding rot13 no no
Encoding unicode escape no no
Encoding url escape no no

Reading the two columns

They are kept apart for the same reason cve-validation.md keeps them apart: a subtype that is neutralized and undetected is a different situation from one that is detected and unneutralized, and the asymmetry is the useful output.

fullwidth is the standing example of the first. canonicalize folds it and has_anomalies stays quiet, because #633 spared the block: NHK is how a Japanese broadcaster is written, and a detector that fires on it is one a CJK-facing caller switches off entirely. A caller who screens without rewriting gets nothing for that row.

The Encoding family is the standing example of a whole family out of scope. disarm operates on the string it is given; a base64 payload is an ordinary run of ASCII letters to every transform here. Decode first, then pass the result in — the same ordering THREAT_MODEL.md gives for the rest of that class.

Structural attacks are mostly out of scope, and that is the honest reading

Four of the seven structural rows are no in both columns. Reordering words, deleting characters and wrapping each letter in brackets are all operations on ordinary ASCII; there is nothing character-level for disarm to act on, which is the same boundary THREAT_MODEL.md draws for word-substitution adversarial examples and GCG suffixes.

The three that are neutralized — fragmentation, spacing injection, whitespace steganography — are neutralized because they inject whitespace or invisible characters, not because the structural manipulation was understood.

Reproducing

pytest tests/test_adversarial_corpora.py

The vectors are in that file. The table above is parsed out of this page and compared against the library row by row, so a cell cannot go stale: every doc gate in this repo parses fenced code blocks and none read a markdown table before this one, which is how a grapheme_len cell stayed wrong through #708.