https://rankdblp.com/sitemap-1.xml.gz https://rankdblp.com/sitemap-2.xml.gz https://rankdblp.com/sitemap-3.xml.gz https://rankdblp.com/sitemap-4.xml.gz https://rankdblp.com/sitemap-5.xml.gz https://rankdblp.com/sitemap-6.xml.gz https://rankdblp.com/sitemap-7.xml.gz https://rankdblp.com/sitemap-8.xml.gz https://rankdblp.com/sitemap-9.xml.gz https://rankdblp.com/sitemap-10.xml.gz https://rankdblp.com/sitemap-11.xml.gz SimilarityDB: features and roadmap

SimilarityDB: what makes it different

A summary of SimilarityDB's distinctive design decisions and features, collected from the repository (README, docs/, sql/, and the Rust crates featurizer/, engine/, frontend/, watermark/). Each section points to the source for details.


1. At a glance

What it is An academic, non-commercial platform for binary code similarity, lineage and clustering. It covers PE, ELF and Mach-O on x86, x86-64, ARM32, ARM64 and RISC-V
Analysis core IDA Pro 9.5 (idalib) + Hex-Rays, run once per sample
Permanent record A versioned, content-addressed IR (<sha256>.ir.jsonl.gz). Binaries can be deleted after analysis
Storage ClickHouse, with a content / references split for every code table
Compute Three static, dependency-pinned Rust binaries: featurizer, similarity engine, web UI
Philosophy Deterministic and explainable. No ML, no LLMs, no deobfuscation
Scale target 10⁹ binaries, about 10¹² function occurrences, about 10¹⁰ distinct functions
sample ─► start.py ─► format extractors (PE/ELF/Mach-O, hashes, YARA, CAPA, DIE, malcontent, IOCs)
              │                                   └─► similaritydb_* tables
              └─ --decompile ─► IDA 9.5 + Hex-Rays (isolated child process)
                                   └─► IR (archived, the permanent record)
                                         └─► similaritydb-featurize (Rust) ─► code_ida_* (content / references)
                                                 └─► similaritydb-engine (Rust) ─► similarity_* tables
                                                         └─► similaritydb-web (Rust, no JS)

2. Technology choices and why

Choice Why (as documented)
ClickHouse Columnar storage with ZSTD codecs. ReplacingMergeTree makes re-inserting identical content free and lets reprocessed data replace old rows. Skip indexes (bloom_filter, tokenbf_v1, ngrambf_v1, minmax, set) support hash, name and string search. Null engine + materialized views fold rows on ingest. AggregatingMergeTree with uniqState keeps prevalence counts
IDA Pro 9.5 + Hex-Rays (idalib) Exports native disassembly, CFG, microcode at MMAT_PREOPTIMIZED (stored as llil) and MMAT_LVARS (stored as mlil), and pseudocode / ctree (stored as decompilation)
Versioned IR as the source of truth Only the IDA stage needs a license. Every feature can be recomputed from archived IR in seconds per sample, with no IDA, when an algorithm changes (scripts/reingest_from_ir.py, bench/refeaturize.py)
Rust featurizer About 100× faster than the Python reference, with output byte-identical to it (parity checked by scripts/featurizer_parity.py, 100% on 17 samples). SHA-256, MurmurHash3 and TLSH are implemented in the crate, with SHA/AVX2 fast paths detected at run time. Measured cost: 1.42 ms CPU per function
Dependency-free Rust engine No crates at all: std::thread::scope, plain HTTP to ClickHouse (RowBinary in, TabSeparated out), hand-written JSON, and a custom thread-caching allocator, because musl's malloc serialized the matching threads
Static musl binaries Pinned toolchain (Rust 1.99.1), committed Cargo.lock, linked with rust-lld. The Dockerfile builds them in a first stage, and IDA is mounted from the host at runtime
Python ingestion Modular extractors, spawn multiprocessing, swap monitoring and pool restarts, and S3/MinIO, date-range, catalog and Nomad job modes
Deterministic magic-byte file typing Replaced Magika, which is an ML classifier, to keep the pipeline ML-free (similaritydb/filetype.py)

3. Storage model: content vs references (dedup + counts)

This is the central storage idea (docs/TOOLING.md, sql/code_ida_schema.sql).

Every code table family is split in two:

kind key holds rows
*_content hash of all stored content columns function text, metrics, features once per distinct function, across all samples
*_references (sample sha256, function address) address, name, flags, links to content hashes once per function per sample

A library routine found in 10,000 samples is stored once in each content table and 10,000 times as a small reference row.

Families: decompiled (pseudocode), disassembled, low-level microcode, function features (v2), CFG, plus per-sample code_ida_sample_stats, code_ida_call_edges and code_ida_strings_by_binary.

Rule: a content key must hash every column of its row, so different functions can never overwrite each other. Anything that depends on addresses or on the sample (names, flags, callee addresses, string offsets) belongs on the references side.

Reference counts and prevalence

Because content and references are separate, "how common is this code?" is cheap to answer:

Per-sample statistics and commit marker

code_ida_sample_stats records functions_total, functions_user, functions_library, functions_thunk, functions_external, functions_small, functions_decompiled, functions_failed, and name sources (names_symbol, names_flirt, names_goresym, names_dummy). It also records code coverage (exec_bytes, user_function_bytes, orphan_code_bytes, coverage_ratio), arch and platform, the packed flag, and every tool version (IDA, Hex-Rays, IR, featurizer).

It is written last and acts as the commit marker: a sample counts as analysed only when its newest row has status ok, so a half-finished export is retried. Re-ingesting a sample first deletes its old per-sample rows. Shared content rows are kept.


4. "Keep and flag", never filter

Every IDA function is exported with flags: type (USER / LIBRARY / THUNK / EXTERNAL / UNKNOWN), small, thunk, library (FLIRT, lib_verified), and name source. Nothing is dropped at extraction. Filtering decisions belong to the consumer. For example, the engine down-weights library code in lineage mode, but still reports library_matches.


5. Feature families (featurizer v1 → v2 → v3 + pseudocode analyses)


6. TLSH: computed and stored, deliberately not used for matching

TLSH (Trend Micro Locality Sensitive Hash) is a fuzzy digest built from bucketed counts of byte triplets in a sliding window. It comes with a distance score.

Implementation. featurizer/src/tlsh.rs is a pure-Rust port of the Apache-2.0 reference implementation, matching the py-tlsh 4.7.2 tlsh.hash() configuration: 128 buckets, 1-byte checksum, "T1" prefix, at least 50 bytes of input, non-conservative. Its output is byte-identical to the Python pipeline. The license note is in featurizer/NOTICE.

Where it is stored. Each digest is a Nullable(FixedString(72)) column:

column input table(s)
tlsh_disassembly native disassembly text disassembled references, similarity metrics, llil references
tlsh_llil / tlsh_llil_new low-level microcode (MMAT_PREOPTIMIZED) similarity metrics, llil references (bloom-filter indexed)
tlsh_instruction_typed_llil typed low-level microcode similarity metrics
tlsh_mlil_skeleton, tlsh_mlil_typed MMAT_LVARS microcode (skeleton / typed) similarity metrics
cfg_feature_tlsh per-block CFG feature vectors in BFS order code_ida_cfg_functions

The web UI shows all of them under "fuzzy hashes" on the function page (frontend/src/functions.rs).

Why it is not a matcher. - In the clustering prototype's ablation (docs/similarity-clustering-draft.md), a TLSH tier (tlsh_llil distance ≤ 30) carried 64% of inter-family matched weight but only 5% of intra-family weight. It raised the noise floor between unrelated packages from 0.005 to 0.043 (max 0.103), so it was switched off. - Function-level token strings are short and low-entropy, which is the worst case for this kind of digest. This agrees with Pagani et al. (2018) (docs/literature-review.md §3.4). - TLSH has no index: at a scale of 10⁹ samples it could only check candidate pairs that another tier already proposed, never generate them. The draft rejects adding more TLSH variants for this reason. - On the benchmark it is weak across architectures, for example tlsh_llil P@1 is 0.05 on the arch axis (bench/RESULTS.md). TLSH of disassembly is classed as arch-specific (docs/featurizer-v2.md §8). - The feature critique notes the redundancy: TLSH appears in three tables, and tlsh_llil_new duplicates tlsh_llil.

The literature review leaves one question open: whether TLSH could break ties for large functions only, where its statistics mean something.


7. Abstraction levels: from raw bytes to semantics

The idea that recurs across the whole design is to describe the same function at several levels of abstraction. Each level has its own hashes, and the engine prefers the most concrete level that still matches. A lower level is more precise; a higher level survives more change (compiler, flags, architecture, platform).

7.1 The IR ladder (what IDA exports)

level stored as survives arch class
native disassembly + CFG disassembly, cfg nothing beyond relinking arch-specific
Hex-Rays microcode MMAT_PREOPTIMIZED llil relocation, renaming measured not neutral (same arch only)
Hex-Rays microcode MMAT_LVARS mlil ISA change (P@1 0.65–0.67 across ISAs) neutral
pseudocode / ctree decompilation ISA and platform neutral

7.2 Normalization inside each level

7.3 How the engine uses the ladder

Tier order puts concrete before abstract: exact → norm → dnorm → pic → bset → struct → MinHash tiers → blocks → consts → api → fused → callgraph. The greedy 1:1 assignment takes the best tier first. Each match records its tier and quality, and a separate neutral quality that counts only the arch-neutral levels. That neutral quality is what the cross-architecture scores (xarch_*) are built from.


8. Similarity engine: tiered, explainable matching

engine/README.md documents the engine.


9. Web UI: similaritydb-web


10. Output watermarking (provenance)

watermark/ and docs/watermarking.md cover this.


11. Robustness and engineering discipline


12. Classic feature extraction (non-code)

Alongside code similarity, per-format extractors fill the similaritydb_* tables:


SimilarityDB: next moves (an engine-style projection)

This roadmap is planned the way a chess engine picks a move. It first evaluates the current position. Then it generates candidate moves, scores each one, and looks several moves ahead for the line that leaves the strongest position. Lines that lose material are pruned.

Every move has to be legal under the project's own rules (docs/literature-review.md §1.2):

Tags used below: - [repo]: the repository already names this as a gap or open question. - [new]: a proposal that does not appear in the repository.


1. Evaluating the position

Strengths (material we have): - content / references storage, which gives deduplication and cheap prevalence counts; - the IR archive, so any feature can be recomputed without IDA; - 13 explainable matching tiers, with lineage and reuse modes, containment and cross-architecture scores; - a benchmark that covers optimization level, architecture, platform and stripping; - a hardened UI, watermarking and fuzzing.

Weaknesses (where the opponent attacks):

weakness evidence
Recall across optimization levels (-O0 against -O2) is the weakest axis engine recall 0.53 opt vs 0.84 arch vs 0.98 platform (engine/README.md)
Name leakage: decomp_norm, disasm_norm and pic keep real callee and global names, so a stripped build cannot match its unstripped twin on these tiers bench/RESULTS.md §6.5 [repo]
Statically linked CRT code makes every Windows build look related bench/RESULTS.md §6.7 [repo]
No compiler axis (clang only; no gcc or MSVC) bench/RESULTS.md [repo]
The engine runs as a batch: all samples, all candidate pairs, every run. Its memory grows to 5.5 GB at 4,000 samples docs/performance.md
LSH has no band table in ClickHouse ("the only big win … is a schema change") docs/performance.md [repo]
The IR is stored per sample, not content-addressed per function docs/pseudocode-analyses.md "IR storage at scale (to implement)" [repo]
Scores are not calibrated: there is no p-value or bit count for "these two samples are related" literature review §3.10 [repo]
Every hash depends on the IDA version clustering draft, open question 3 [repo]

Evaluation: the position is strong. The project has excellent foundations and measurement, but three structural problems block the 10⁹ target: the batch engine, the missing band table, and per-sample IR. Precision has two cheap leaks: name leakage and CRT code. Fix the cheap leaks first. Then rebuild the foundation for scale, and only after that add features.


2. Candidate moves, scored

Columns: - Gain: expected value, 1 to 5. - Cost: effort, 1 (days) to 5 (months). - Risk: chance it doesn't pay off, low / mid / high. - Unlocks: the moves that become possible afterwards. In chess terms this is tempo: a move that opens many others is worth more than its own gain.

A. Forcing moves: cheap, immediate precision

# move gain cost risk unlocks
A1 Name-free normalizers [repo]: replace every non-import callee or global name with a placeholder, and map imports to normalized API names, in decomp_norm, disasm_norm and pic 4 1 low stripped ↔ unstripped matching on exact tiers; A2
A2 CRT alias folding [repo]: fold __mingw_*, __stdio_common_*, glibc __*_chk and Darwin $INODE64 onto their POSIX names 2 1 low cross-platform api tier
A3 Toolchain library auto-flagging [repo]: a function present in more than X% of the samples of one (platform, toolchain) is flagged LIB_INFERRED 4 2 mid clean Windows lineage; C1
A4 Information-content weight for exact tiers [repo, open question]: q = 1 becomes q·min(1, log2(N/df)/b₀), so a trivially small dnorm collision stops counting as full evidence 3 1 low D1
A5 Fix watermark bug W1 [repo] 1 1 low -

B. Development: the foundation for scale

# move gain cost risk unlocks
B1 LSH band table in ClickHouse [repo]: code_ida_lsh_bands(tier, band_id, band_value, features_hash), ordered by (tier, band_id, band_value). Candidate lookup becomes an indexed range read 5 2 low B2, E1, E3
B2 Incremental engine [new]: similaritydb-engine add --samples NEW queries only the new samples against the band table and exact-key indexes, scores only the pairs that involve them, and updates clusters with incremental union-find. A full batch run becomes a rare, periodic rebuild 5 4 mid near-real-time clustering; E2, E4
B3 Content-addressed IR store [repo, designed]: ir/fn/<hh>/<hash>.json.zst plus per-sample manifests. Re-featurization runs once per distinct function, and storage shrinks by the dedup ratio 4 3 low cheap featurizer upgrades; B4
B4 Featurizer content cache [repo, mentioned in perf]: skip featurizing any function whose content hash already has a row at the current featurizer version 4 2 low well under the projected 394k core-hours
B5 Per-IDA-version keys [repo, open question]: store each content key alongside (ida_version, hexrays_version), plus a differential study across IDA releases 3 2 mid safe IDA upgrades
B6 Run retention and TTL [repo]: old engine runs are never deleted today 1 1 low -

C. Better evidence: recall where we are weakest

# move gain cost risk unlocks
C1 Stronger MMAT_LVARS normalization for -O0 [repo]: remove stack spills and reloads and copy-only temporaries, and canonicalize address arithmetic 4 3 mid opt-axis recall; benefits every neutral tier
C2 Logical-function features [repo, P1.2]: inline small callees and thunk chains before hashing (IDA_INLINE_SMALL_HELPERS exists but is opt-in and unmeasured) 3 2 mid robustness against function splitting and outlining
C3 Call-graph context key [repo, open question]: an exact tier on hash(key, multiset of callee keys, multiset of caller keys) 3 2 mid propagation seeds
C4 v3 rarity tier [repo, planned]: crypto_tags, rare string_skeletons and rare constants as a high-precision, low-recall tier 3 1 low family signatures (E3)
C5 String references by text [repo]: string_refs stores only offsets today 2 1 low C4
C6 Deterministic I/O hashing [repo, open question]: run MMAT_LVARS microcode on fixed input vectors and hash the outputs, Pewny-style 4 5 high the cross-compiler axis

D. Measurement: knowing what we've won

# move gain cost risk unlocks
D1 Calibrated significance [repo, open question]: express each sample-pair score as bits or an e-value against a null corpus of unrelated pairs. The UI then shows "1 in 10⁹ by chance" rather than "0.31" 5 3 mid thresholds that mean the same everywhere; E4
D2 Compiler axis [repo]: rebuild the benchmark corpus with gcc and MSVC (clang-cl) 4 2 low honest numbers; tuning C1 and C6
D3 Held-out threshold evaluation [repo]: freeze the thresholds, then score a corpus they were not tuned on, reporting P@k, MRR and PR curves 3 1 low trustworthy defaults
D4 Adversarial axis [repo, open question]: O-LLVM / Tigress-style transformations (bogus control flow, splitting, substitution). We measure how much we degrade; we don't deobfuscate 3 2 low known limits
D5 Ground-truth capture in the UI [new]: an admin-only "confirm / reject" on function and sample matches, stored as labels. These are used only for evaluation, never for training 3 2 low D3 on real data

E. Attack: new capabilities for analysts

# move gain cost risk unlocks
E1 Two-sample diff view [new, data exists]: a BinDiff-style page with matched, unmatched and changed functions, side-by-side pseudocode, and statement-level diffs from the statements analysis hash lists 5 2 low patch and variant analysis
E2 Cluster history [new]: track cluster ids across engine runs as birth, growth, merge, split and death events, with a timeline per cluster 4 2 low E5
E3 Feature-level, arch-neutral family signatures [repo, open question]: from a cluster's stable core (functions in every member), emit a rule over block hashes, API-argument tags and rare constants. Ship a matcher (similaritydb-sig scan), and also emit YARA where byte patterns exist. Evaluate with leave-one-out 5 4 mid detection, not only clustering
E4 Directed lineage / phylogeny [repo, open question]: orient edges with containment asymmetry, compile timestamps and first-seen dates, then build a minimum-cost arborescence per cluster ("v2.1 → v2.2 added module X") 4 3 high family evolution reports
E5 Watchlists [new]: "notify when a new sample joins cluster C or contains function F". Results are derived from the latest runs, the same way analysis jobs are derived today 3 2 low operational use (needs B2)
E6 Module recovery [repo, open question]: group functions into modules (BCD-style: call graph, data refs, address locality), then compute module-level df and containment ("contains zlib inflate 1.2.x") 4 4 high readable explanations; better library suppression
E7 Pairwise explanation page [new, data exists]: a deterministic narrative of the per-tier counts and weights stored for each pair, plus the top contributing functions and why each counted 3 1 low trust in scores; D5
E8 Read-only JSON API [new]: the same parameterized queries as the UI, behind the same limits and the read-only ClickHouse user 3 2 low integrations (MISP / STIX export of clusters)
E9 Metadata similarity channel [repo, design rule]: imphash, Rich header, telfhash and Authenticode signer as a separate, labelled relation, never mixed into code scores 2 1 low pivoting in the UI
E10 Query by snippet (CLI) [new]: similaritydb-query --bytes FILE --arch x86-64 lifts a code fragment with the same IDA/IR pipeline and runs it against the band table 3 3 mid hunting from a report or a memory dump

3. The principal variation: the line I would play

The search goes about 8 moves deep. Order matters: each move opens the next.

 1. A1  name-free normalizers           ── forcing: stops a precision leak
 2. A3  toolchain library flagging       ── forcing: unblocks Windows lineage
    (A2, A4, A5 in the same sprint: one-day moves)
 3. D2  gcc + MSVC benchmark             ── measure before the big work
 4. B1  LSH band table                   ── the foundation every later move needs
 5. B3+B4 content-addressed IR + cache   ── makes featurizer upgrades cheap
 6. B2  incremental engine               ── from batch to online; the key move
 7. E1  diff view  +  E7 explanations    ── visible payoff from existing data
 8. D1  calibrated significance          ── scores with a meaning
 9. E2  cluster history → E5 watchlists  ── operational product
10. E3  arch-neutral family signatures   ── from clustering to detection

Why this order: - Moves 1–2 are forcing: cheap, low risk, and every later measurement gets better. Tuning anything else while name leakage and CRT noise are in the data would tune against artifacts. - Move 3 comes before C1 and C6, so the opt-axis and compiler work have a target to hit. - Moves 4–6 are development. Without them every new tier makes the batch run slower, so the position gets worse with each feature added. B1 is cheap and is a prerequisite for B2, E3 and E10. - Move 7 cashes in data that already exists: per-pair tier counts, every function match, and statement hash lists. Users see value while the deep work continues. - Moves 8–10 turn SimilarityDB from "a similarity database" into "a lineage and detection system". Signatures (E3) need the stable cores that cluster history (E2) provides.

Best evaluation after the line: the engine is online at 10⁹ scale; Windows lineage is clean; recall across compilers has been measured, and its weaknesses are known; similarity has a calibrated meaning; analysts get diffs, explanations, cluster timelines, alerts and architecture-neutral signatures.

Side line (run in parallel, research): C1 (-O0 normalization), then C3 / C4 (context and rarity tiers), then D4 (adversarial axis). Each is measured on the benchmark from move 3 before it is promoted to a default tier.


4. Lines considered and pruned

line why pruned
Learned embeddings (Gemini, Asm2Vec, jTrans) and an approximate-nearest-neighbour index Illegal move: breaks the no-ML / explainability rule
LLM-written match explanations Illegal: E7 gives deterministic explanations instead
More TLSH variants, or TLSH as a tier Already refuted: no index (O(n²) at scale), and it carried 64% of inter-family weight. At most an experiment as a tie-breaker for large functions only
Deobfuscation passes (e.g. gooMBA) Out of scope by design (removed in 5a08df9). D4 measures degradation instead
Dynamic analysis / sandbox traces Binaries are deleted after analysis, and it isn't deterministic. A different product
A second lifter (e.g. Ghidra P-code) as a cross-check Promising but expensive, and it can only run at ingestion time. Revisit after B5 shows how unstable IDA versions are
Network alignment (QBinDiff) instead of greedy matching + propagation Unbounded cost per pair at scale. At most a per-pair refinement in the diff view (E1)
Per-family thresholds learned from labels Drifts toward ML. D1's calibration gives one meaningful global threshold

5. Threats to watch (the opponent's best replies)


6. Feature projection beyond the line (moves 11+)

These are less certain and depend on how moves 1–10 turn out: