Skip to content

fix(engine): bound request-supplied sizes so huge values can't abort the server - #287

Merged
jsam merged 2 commits into
mainfrom
fm/fix-size-clamps
Oct 5, 2026
Merged

jsam merged 2 commits into
mainfrom
fm/fix-size-clamps

Conversation

@jsam

@jsam jsam commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Intent

I want you to really hyper focus on making the symbolic engine, you know, crisp and really truly production ready. (Context: the adversarial audit found a P0: one query with a huge client-supplied size (e.g. lsh_probes = 3e17) makes the engine try to allocate that much memory and abort; hnsw_nearest ef_search and top_k have the same class.)

The audit's P0 item, by reference: on an empty knowledge graph with +a[(1)], lsh_probes with num_probes=4 returns [0,1,2,4], but num_probes=300000000000000000 closes the connection and kills the server process ("memory allocation of 2400000000000000000 bytes failed"). lsh_probes(bucket, num_hp, num_probes) calls Vec::with_capacity(num_probes) with the client's value; an allocation beyond the address space makes Rust abort, which cannot be caught. The optional ef_search argument of hnsw_nearest (parsed as any usize, reaching BinaryHeap::with_capacity in hnsw_rs once an index has over 1,024 rows) and top_k<K, ...> (BinaryHeap::with_capacity(k+1)) are the same class. Any principal that may run a query can do this, and the memory budgets cannot intercept it because the abort happens before any accounting. The audit's fix: clamp num_probes (to the number of distinct probes), ef_search and top_k's K at parse or evaluation time and reject larger values with validation, and audit every with_capacity whose argument comes from a value.

What Changed

  • Added src/size_limits.rs with upper bounds: top_k/top_k_threshold K ≤ 1,000,000; hnsw_nearest k and ef_search ≤ 10,000; ef_construction ≤ 10,000; lsh_probes/lsh_multi_probe num_probes ≤ 39,774 (every distinct probe for 62 bits). The parser now rejects literals over these bounds, and hnsw_nearest ef_search = 0, with a validation error that names the parameter and its bound. Index creation now rejects ef_construction/ef_search outside 1–10,000.
  • Allocations that take their size from a request are now capped by what the input can actually produce. LSH probe vectors are capped at the number of distinct probes, top-k heaps at the group size, and the HNSW search ef at the entry count. HNSW fetch doubling now saturates instead of overflowing. Saved index definitions are clamped to safe m/ef_construction when the graph is rebuilt. concat/replace return null when the result would exceed 16 MiB. Request deadlines use checked_add, so a huge timeout means no deadline instead of a panic.
  • Updated the indexing, aggregations, commands and functions docs (and the bundled GUI docs) with the new bounds. Added unit tests and tests/size_limit_tests.rs covering the oversized values from the audit.

Risk Assessment

✅ Low: The fix round removed the LSH dimension cap cleanly from the constant, the five evaluator checks and the docs. Every other clamp is still in place: num_probes, ef_search, top_k and hnsw k are rejected at parse time and capped at evaluation time, and saved HNSW parameters are clamped. No new defects were found in the changed code.

Testing

I built the server and client binaries and ran the real server on a free local port with a disposable data dir. I then sent IQL scripts over WebSocket with the real client: the audit's lsh_probes repro, runtime-bound probe counts, hnsw_nearest k and ef_search on a 1,100-row index (above the 1,024-row graph-walk threshold), index ef parameters, top_k and top_k_threshold, the oversized string builders, and 20,000-dimension LSH. Every live scenario behaved as intended, and the same server PID answered queries afterwards. The targeted size_limit_tests integration file also passes, but its fixed-seed fuzz runs against an in-process handler, so it is recorded as untested live. I did not rebuild the base commit to reproduce the original crash live. Everything I set up was torn down; only the evidence files remain.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Audit repro: lsh_probes(0,4,4) returns [0,1,2,4]; num_probes=3e17 is refused with a validation error and the server keeps serving ✅ pass live Audit P0 repro artifact; pgrep showed server PID 1413287 alive afterwards
Adversarial: num_probes of 3e17 read from a stored fact (not a literal) yields only the 15 distinct probes for lsh_probes and 8 for lsh_multi_probe, without aborting ✅ pass live s2_transcript.txt
lsh_multi_probe literal 3e17 and lsh_probes one over the 39,774 bound are refused with errors that name the bound ✅ pass live s2_transcript.txt
hnsw_nearest on a 1,100-row index: k=3e17, ef_search=3e17, 10001 and 0 refused; ef_search=10000 returns 5 neighbours ✅ pass live s2_transcript.txt
.index create with ef_construction 10001 or ef_search 3e17 is refused with an index error naming the bound ✅ pass live s2_transcript.txt
top_k<1000001> and top_k_threshold<3e17> refused; top_k<1000000> over 3 rows returns 3 rows ✅ pass live s2_transcript.txt
User decision: a 20,000-dimension embedding still gets an LSH bucket and multi-probe results (not null) ✅ pass live s3_transcript.txt
Oversized replace (empty pattern) and 6-deep nested concat return null instead of allocating; normal replace still returns 'ayyc' ✅ pass live s3_transcript.txt
Fixed-seed fuzz of every size position (boundaries, u64::MAX, random) never takes the handler down ⏸️ untested no The prior payload did not establish a live result for this scenario: the fuzz ran only through cargo test --test size_limit_tests against an in-process Handler, not the running server binary. To dri…
Evidence: Live client transcript: runtime probes, HNSW, index params, top_k
Connecting to server at http://127.0.0.1:27493...
Connected!

Server status: healthy
Authenticated as: admin
Current knowledge graph: default

> +np[(300000000000000000)]
Inserted 1 fact(s) into 'np'.
> ?np(N), P = lsh_probes(0, 4, N)
┌────────────────────┬──────────────────────────────────────────┐
│ N                  │ P                                        │
├────────────────────┼──────────────────────────────────────────┤
│ 300000000000000000 │ [0.0, 1.0, 2.0, 4.0, 8.0, 3.0, 5.0, 9.0… │
└────────────────────┴──────────────────────────────────────────┘
1 rows
> ?np(N), P = lsh_multi_probe([1.0, -0.5], 0, 3, N)
┌────────────────────┬──────────────────────────────────────────┐
│ N                  │ P                                        │
├────────────────────┼──────────────────────────────────────────┤
│ 300000000000000000 │ [0.0, 1.0, 4.0, 2.0, 5.0, 3.0, 6.0, 7.0] │
└────────────────────┴──────────────────────────────────────────┘
1 rows
> ?a(X), P = lsh_multi_probe([1.0, 0.5], 0, 8, 300000000000000000)
Parse error: lsh_multi_probe: num_probes must be at most 39774, got 300000000000000000
> ?a(X), P = lsh_probes(0, 62, 39775)
Parse error: lsh_probes: num_probes must be at most 39774, got 39775
> +docs(id: int, emb: vector)
Schema for 'docs' registered with 2 columns (persistent)
> +docs[(0, [0.0, 1.0, 1.0]), (1, [0.8414709848078965, 0.5403023058681398, 1.0]), (2, [0.9092974268256817, -0.4161468365471424, 1.0]), (3, [0.1411200080598672, -0.9899924966004454, 1.0]), (4, [-0.7568
Inserted 1100 fact(s) into 'docs'.
> .index create doc_idx on docs(emb) metric euclidean
Index 'doc_idx' created on docs.emb (1100 vectors).
> ?hnsw_nearest("doc_idx", [0.0, 1.0, 1.0], 300000000000000000, Id, D)
Parse error: hnsw_nearest: k must be at most 10000, got 300000000000000000
> ?hnsw_nearest("doc_idx", [0.0, 1.0, 1.0], 5, Id, D, 300000000000000000)
Parse error: hnsw_nearest: ef_search must be at most 10000, got 300000000000000000
> ?hnsw_nearest("doc_idx", [0.0, 1.0, 1.0], 5, Id, D, 0)
Parse error: hnsw_nearest: ef_search must be a positive integer, got '0'
> ?hnsw_nearest("doc_idx", [0.0, 1.0, 1.0], 5, Id, D, 10000)
┌──────┬────────────────────────┐
│ Id   │ D                      │
├──────┼────────────────────────┤
│    0 │                    0.0 │
│  333 │   0.008821251802146435 │
│  377 │   0.008881540037691593 │
│  710 │ 0.00006028870848240331 │
│ 1043 │   0.008760964497923851 │
└──────┴────────────────────────┘
5 rows
> ?hnsw_nearest("doc_idx", [0.0, 1.0, 1.0], 5, Id, D, 10001)
Parse error: hnsw_nearest: ef_search must be at most 10000, got 10001
> .index create i2 on docs(emb) ef_construction 10001
Index error: HNSW parameter ef_construction must be between 1 and 10000, got 10001
> .index create i3 on docs(emb) ef_search 300000000000000000
Index error: HNSW parameter ef_search must be between 1 and 10000, got 300000000000000000
> +scores[("a", 1), ("b", 2), ("c", 3)]
Inserted 3 fact(s) into 'scores'.
> best(top_k<1000001, N, S:desc>) <- scores(N, S)
Parse error: top_k: k must be at most 1000000, got 1000001
> best2(top_k_threshold<300000000000000000, 0.5, N, S:desc>) <- scores(N, S)
Parse error: top_k_threshold: k must be at most 1000000, got 300000000000000000
> best3(top_k<1000000, N, S:desc>) <- scores(N, S)
┌─────┬───┐
│ X   │ Y │
├─────┼───┤
│ "a" │ 1 │
│ "b" │ 2 │
│ "c" │ 3 │
└─────┴───┘
3 rows
(session: 1 rule(s))
> ?best3(N, S)
┌─────┬───┐
│ N   │ S │
├─────┼───┤
│ "a" │ 1 │
│ "b" │ 2 │
│ "c" │ 3 │
└─────┴───┘
3 rows
> ?a(X)
┌───┐
│ X │
├───┤
│ 1 │
└───┘
1 rows
Evidence: Live client transcript: high-dim LSH and string builders
Connecting to server at http://127.0.0.1:27493...
Connected!

Server status: healthy
Authenticated as: admin
Current knowledge graph: default

> ?a(X), B = lsh_bucket([<…>, 1.0], 0, 16)
┌───┬───────┐
│ X │ B     │
├───┼───────┤
│ 1 │ 22321 │
└───┴───────┘
1 rows
> ?a(X), P = lsh_multi_probe([<…>, 1.0], 0, 8, 5)
┌───┬─────────────────────────────────┐
│ X │ P                               │
├───┼─────────────────────────────────┤
│ 1 │ [49.0, 51.0, 57.0, 33.0, 177.0] │
└───┴─────────────────────────────────┘
1 rows
> +s[("x…")]
Inserted 1 fact(s) into 's'.
> ?s(S), R = replace(S, "", S), L = 1
┌──────────────────────────────────────────┬──────┬───┐
│ S                                        │ R    │ L │
├──────────────────────────────────────────┼──────┼───┤
│ "x…… │ null │ 1 │
└──────────────────────────────────────────┴──────┴───┘
1 rows
> ?s(S), R = concat(concat(concat(concat(concat(concat(S, S, S), concat(S, S, S), concat(S, S, S)), concat(concat(S, S, S), concat(S, S, S), concat(S, S, S)), concat(concat(S, S, S), concat(S, S, S), 
┌──────────────────────────────────────────┬──────┐
│ S                                        │ R    │
├──────────────────────────────────────────┼──────┤
│ "x…… │ null │
└──────────────────────────────────────────┴──────┘
1 rows
> ?a(X), R = replace("abc", "b", "yy")
┌───┬────────┐
│ X │ R      │
├───┼────────┤
│ 1 │ "ayyc" │
└───┴────────┘
1 rows
> ?a(X)
┌───┐
│ X │
├───┤
│ 1 │
└───┘
1 rows
Evidence: Server log for the live run
InputLayer Server
=================

�[2m2026-10-04T20:50:26.062644Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m data_dir_lock_acquired �[3mdata_dir�[0m�[2m=�[0m/tmp/tmp.Tv8f8vncYp/data �[3melapsed_us�[0m�[2m=�[0m155
�[2m2026-10-04T20:50:26.067402Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_load_complete �[3mknowledge_graphs�[0m�[2m=�[0m0 �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:26.077306Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m9
�[2m2026-10-04T20:50:26.077357Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_create_complete �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m9
�[2m2026-10-04T20:50:26.078447Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_memory_limits �[3mquery_memory_bytes�[0m�[2m=�[0m4294967296 �[3mtotal_query_memory_bytes�[0m�[2m=�[0m0 �[3mcompute_permits�[0m�[2m=�[0m24
�[2m2026-10-04T20:50:26.081354Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T20:50:26.081382Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_create_complete �[3mkg�[0m�[2m=�[0m_internal �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T20:50:26.512577Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::program_commit�[0m�[2m:�[0m program_commit_persisted �[3mkg�[0m�[2m=�[0m_internal �[3mrelations�[0m�[2m=�[0m1 �[3mstatements�[0m�[2m=�[0m1 �[3mtime�[0m�[2m=�[0m1 �[3mpersist_ms�[0m�[2m=�[0m4
�[2m2026-10-04T20:50:26.512764Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m snapshot_publish_complete �[3mrelations�[0m�[2m=�[0m1 �[3mrules�[0m�[2m=�[0m0 �[3msnapshot_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:26.512801Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Auth bootstrap: admin user created
�[2m2026-10-04T20:50:26.520756Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::program_commit�[0m�[2m:�[0m program_commit_persisted �[3mkg�[0m�[2m=�[0m_internal �[3mrelations�[0m�[2m=�[0m3 �[3mstatements�[0m�[2m=�[0m1 �[3mtime�[0m�[2m=�[0m2 �[3mpersist_ms�[0m�[2m=�[0m7
�[2m2026-10-04T20:50:26.521031Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m snapshot_publish_complete �[3mrelations�[0m�[2m=�[0m4 �[3mrules�[0m�[2m=�[0m0 �[3msnapshot_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:26.521081Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Auth bootstrap: API key 'bootstrap' created for admin
�[2m2026-10-04T20:50:26.522540Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Auth bootstrap: credentials saved to /tmp/tmp.Tv8f8vncYp/data/credentials.toml

=== INITIAL ADMIN CREDENTIALS CREATED ===
Admin API key: ****6238
==========================================

Generated credentials saved to: /tmp/tmp.Tv8f8vncYp/data/credentials.toml
Retrieve them with:  cat /tmp/tmp.Tv8f8vncYp/data/credentials.toml
Delete this file to generate new credentials on next boot.

Storage engine initialized

HTTP Server
-----------
Address: 127.0.0.1:27493

HTTP server listening on: http://127.0.0.1:27493
WebSocket API docs at: http://127.0.0.1:27493/api/ws-docs
�[2m2026-10-04T20:50:34.937437Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_connection_start �[3mkg�[0m�[2m=�[0mdefault
�[2m2026-10-04T20:50:34.937687Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m audit_auth_apikey_success �[3musername�[0m�[2m=�[0m"admin" �[3mcredential�[0m�[2m=�[0mapikey:bootstrap#3
�[2m2026-10-04T20:50:34.937718Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_authenticated �[3mkg�[0m�[2m=�[0mdefault �[3musername�[0m�[2m=�[0madmin �[3mrole�[0m�[2m=�[0madmin �[3mcredential�[0m�[2m=�[0mapikey:bootstrap#3
�[2m2026-10-04T20:50:34.937741Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.937785Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_session_created �[3mkg�[0m�[2m=�[0mdefault �[3mactive_sessions�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:34.939512Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_start �[3mprogram_len�[0m�[2m=�[0m7 �[3mprogram_preview�[0m�[2m=�[0m+a[(1)] �[2m�[3mrequest_id�[0m�[2m=�[0m1 �[3mmsg_bytes�[0m�[2m=�[0m47�[0m
�[2m2026-10-04T20:50:34.939788Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_start �[3mprogram_len�[0m�[2m=�[0m7
�[2m2026-10-04T20:50:34.939844Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.939880Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_parse_complete �[3mprogram_len�[0m�[2m=�[0m7 �[3mstatements�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.943920Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::program_commit�[0m�[2m:�[0m program_commit_persisted �[3mkg�[0m�[2m=�[0mdefault �[3mrelations�[0m�[2m=�[0m1 �[3mstatements�[0m�[2m=�[0m1 �[3mtime�[0m�[2m=�[0m3 �[3mpersist_ms�[0m�[2m=�[0m3
�[2m2026-10-04T20:50:34.944062Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m snapshot_publish_complete �[3mrelations�[0m�[2m=�[0m1 �[3mrules�[0m�[2m=�[0m0 �[3msnapshot_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.944137Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_statement_exec_complete �[3mprogram_len�[0m�[2m=�[0m7 �[3mstmt_exec_ms�[0m�[2m=�[0m4 �[3msession_facts�[0m�[2m=�[0m0 �[3msession_rules�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.944178Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_complete_messages �[3mprogram_len�[0m�[2m=�[0m7 �[3mtotal_ms�[0m�[2m=�[0m4
�[2m2026-10-04T20:50:34.944306Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_complete �[3mprogram_len�[0m�[2m=�[0m7 �[3mcompute_ms�[0m�[2m=�[0m4 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m1 �[3mmsg_bytes�[0m�[2m=�[0m47�[0m
�[2m2026-10-04T20:50:34.944356Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_end �[3mprogram_len�[0m�[2m=�[0m7 �[3melapsed_ms�[0m�[2m=�[0m4 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m1 �[3mmsg_bytes�[0m�[2m=�[0m47�[0m
�[2m2026-10-04T20:50:34.945022Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_start �[3mprogram_len�[0m�[2m=�[0m30 �[3mprogram_preview�[0m�[2m=�[0m?a(X), P = lsh_probes(0, 4, 4) �[2m�[3mrequest_id�[0m�[2m=�[0m2 �[3mmsg_bytes�[0m�[2m=�[0m70�[0m
�[2m2026-10-04T20:50:34.945242Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_start �[3mprogram_len�[0m�[2m=�[0m30
�[2m2026-10-04T20:50:34.945267Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.945287Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_parse_complete �[3mprogram_len�[0m�[2m=�[0m30 �[3mstatements�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.945554Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_execute_start �[3mrules�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:34.945723Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_parse_complete �[3msource_len�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.945782Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_specialize_complete �[3msource_len�[0m�[2m=�[0m1 �[3mspec_us�[0m�[2m=�[0m47
�[2m2026-10-04T20:50:34.945890Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_sip_complete �[3msource_len�[0m�[2m=�[0m1 �[3msip_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.945941Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_magic_sets_complete �[3msource_len�[0m�[2m=�[0m1 �[3mmagic_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:34.946035Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_build_ir_complete �[3msource_len�[0

... [71222 bytes truncated] ...

�[2minputlayer�[0m�[2m:�[0m engine_parse_complete �[3msource_len�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.834578Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_specialize_complete �[3msource_len�[0m�[2m=�[0m1 �[3mspec_us�[0m�[2m=�[0m51
�[2m2026-10-04T20:50:56.834645Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_sip_complete �[3msource_len�[0m�[2m=�[0m1 �[3msip_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.834681Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_magic_sets_complete �[3msource_len�[0m�[2m=�[0m1 �[3mmagic_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.834757Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_build_ir_complete �[3msource_len�[0m�[2m=�[0m1 �[3mbuild_ms�[0m�[2m=�[0m0 �[3mir_nodes�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.834914Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_optimize_complete �[3msource_len�[0m�[2m=�[0m1 �[3mopt_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.834939Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_shared_views_complete �[3msource_len�[0m�[2m=�[0m1 �[3mshared_ms�[0m�[2m=�[0m0 �[3mshared_views�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.836112Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_rule_complete �[3msource_len�[0m�[2m=�[0m1 �[3mrule_idx�[0m�[2m=�[0m0 �[3mrule_head�[0m�[2m=�[0m__query__ �[3mrule_ms�[0m�[2m=�[0m1 �[3mrecursive�[0m�[2m=�[0mfalse �[3mworkers�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.836138Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_execute_complete �[3msource_len�[0m�[2m=�[0m1 �[3mtotal_ms�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.836149Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::snapshot�[0m�[2m:�[0m snapshot_execute �[3mprogram_len�[0m�[2m=�[0m54 �[3mrules�[0m�[2m=�[0m1 �[3msession_facts�[0m�[2m=�[0m0 �[3melapsed_ms�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.836189Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_engine_exec_complete �[3mprogram_len�[0m�[2m=�[0m36 �[3mkg�[0m�[2m=�[0mdefault �[3mquery_exec_ms�[0m�[2m=�[0m1 �[3mhas_session_facts�[0m�[2m=�[0mfalse
�[2m2026-10-04T20:50:56.836270Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_complete �[3mprogram_len�[0m�[2m=�[0m36 �[3mtotal_ms�[0m�[2m=�[0m2 �[3mrow_count�[0m�[2m=�[0m1 �[3mtotal_count�[0m�[2m=�[0m1 �[3mtruncated�[0m�[2m=�[0mfalse
�[2m2026-10-04T20:50:56.836407Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_complete �[3mprogram_len�[0m�[2m=�[0m36 �[3mcompute_ms�[0m�[2m=�[0m2 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m6 �[3mmsg_bytes�[0m�[2m=�[0m82�[0m
�[2m2026-10-04T20:50:56.836456Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_end �[3mprogram_len�[0m�[2m=�[0m36 �[3melapsed_ms�[0m�[2m=�[0m2 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m6 �[3mmsg_bytes�[0m�[2m=�[0m82�[0m
�[2m2026-10-04T20:50:56.837182Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_start �[3mprogram_len�[0m�[2m=�[0m5 �[3mprogram_preview�[0m�[2m=�[0m?a(X) �[2m�[3mrequest_id�[0m�[2m=�[0m7 �[3mmsg_bytes�[0m�[2m=�[0m45�[0m
�[2m2026-10-04T20:50:56.837385Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_start �[3mprogram_len�[0m�[2m=�[0m5
�[2m2026-10-04T20:50:56.837408Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.837428Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_parse_complete �[3mprogram_len�[0m�[2m=�[0m5 �[3mstatements�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.837538Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_execute_start �[3mrules�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.837641Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_parse_complete �[3msource_len�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.837688Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_specialize_complete �[3msource_len�[0m�[2m=�[0m1 �[3mspec_us�[0m�[2m=�[0m37
�[2m2026-10-04T20:50:56.837750Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_sip_complete �[3msource_len�[0m�[2m=�[0m1 �[3msip_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.837786Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_magic_sets_complete �[3msource_len�[0m�[2m=�[0m1 �[3mmagic_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.837843Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_build_ir_complete �[3msource_len�[0m�[2m=�[0m1 �[3mbuild_ms�[0m�[2m=�[0m0 �[3mir_nodes�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.837905Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_optimize_complete �[3msource_len�[0m�[2m=�[0m1 �[3mopt_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.837916Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_shared_views_complete �[3msource_len�[0m�[2m=�[0m1 �[3mshared_ms�[0m�[2m=�[0m0 �[3mshared_views�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.838725Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_rule_complete �[3msource_len�[0m�[2m=�[0m1 �[3mrule_idx�[0m�[2m=�[0m0 �[3mrule_head�[0m�[2m=�[0m__query__ �[3mrule_ms�[0m�[2m=�[0m0 �[3mrecursive�[0m�[2m=�[0mfalse �[3mworkers�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.838748Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_execute_complete �[3msource_len�[0m�[2m=�[0m1 �[3mtotal_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:50:56.838758Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::snapshot�[0m�[2m:�[0m snapshot_execute �[3mprogram_len�[0m�[2m=�[0m20 �[3mrules�[0m�[2m=�[0m1 �[3msession_facts�[0m�[2m=�[0m0 �[3melapsed_ms�[0m�[2m=�[0m1
�[2m2026-10-04T20:50:56.838773Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_engine_exec_complete �[3mprogram_len�[0m�[2m=�[0m5 �[3mkg�[0m�[2m=�[0mdefault �[3mquery_exec_ms�[0m�[2m=�[0m1 �[3mhas_session_facts�[0m�[2m=�[0mfalse
�[2m2026-10-04T20:50:56.838837Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_complete �[3mprogram_len�[0m�[2m=�[0m5 �[3mtotal_ms�[0m�[2m=�[0m1 �[3mrow_count�[0m�[2m=�[0m1 �[3mtotal_count�[0m�[2m=�[0m1 �[3mtruncated�[0m�[2m=�[0mfalse
�[2m2026-10-04T20:50:56.838973Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_complete �[3mprogram_len�[0m�[2m=�[0m5 �[3mcompute_ms�[0m�[2m=�[0m1 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m7 �[3mmsg_bytes�[0m�[2m=�[0m45�[0m
�[2m2026-10-04T20:50:56.839007Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_end �[3mprogram_len�[0m�[2m=�[0m5 �[3melapsed_ms�[0m�[2m=�[0m1 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m7 �[3mmsg_bytes�[0m�[2m=�[0m45�[0m
�[2m2026-10-04T20:50:56.839571Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_session_disconnecting �[3mactive_sessions�[0m�[2m=�[0m1
�[2m2026-10-04T20:51:02.976529Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest�[0m�[2m:�[0m Received SIGTERM, shutting down...
�[2m2026-10-04T20:51:02.976954Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest�[0m�[2m:�[0m session_reaper_shutdown
�[2m2026-10-04T20:51:02.977094Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest�[0m�[2m:�[0m auto_compact_shutdown
�[2m2026-10-04T20:51:02.980023Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::program_commit�[0m�[2m:�[0m program_commit_persisted �[3mkg�[0m�[2m=�[0m_internal �[3mrelations�[0m�[2m=�[0m1 �[3mstatements�[0m�[2m=�[0m1 �[3mtime�[0m�[2m=�[0m9 �[3mpersist_ms�[0m�[2m=�[0m2
�[2m2026-10-04T20:51:02.980260Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m snapshot_publish_complete �[3mrelations�[0m�[2m=�[0m4 �[3mrules�[0m�[2m=�[0m0 �[3msnapshot_ms�[0m�[2m=�[0m0
�[2m2026-10-04T20:51:02.980384Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Flushing WAL and saving metadata...
�[2m2026-10-04T20:51:03.167241Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T20:51:03.167296Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Shutdown complete.
Evidence: Audit P0 repro against live server
> ?a(X), P = lsh_probes(0, 4, 4)
│ 1 │ [0.0, 1.0, 2.0, 4.0] │
> ?a(X), P = lsh_probes(0, 4, 300000000000000000)
Parse error: lsh_probes: num_probes must be at most 39774, got 300000000000000000
> ?a(X)
│ 1 │ (server still serving, same PID)
Evidence: size_limit_tests output
     Running tests/size_limit_tests.rs (target/debug/deps/size_limit_tests-046f038e6f6cbf22)

running 9 tests
test lsh_multi_probe_literal_over_bound_is_refused ... ok
test huge_client_timeout_does_not_panic ... ok
test lsh_probes_literal_over_bound_is_refused ... ok
test top_k_over_bound_is_refused ... ok
test lsh_probes_runtime_size_gets_only_distinct_probes ... ok
test index_ef_parameters_over_bound_are_refused ... ok
test string_builders_over_bound_return_null ... ok
test hnsw_nearest_k_and_ef_search_over_bound_are_refused ... ok
test fuzz_size_parameters_never_take_the_handler_down ... ok

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ src/code_generator/mod.rs:2655 - This adds a new rule: the LSH builtins (lsh_bucket, lsh_bucket_int8, lsh_bucket_with_distances, lsh_multi_probe, lsh_multi_probe_int8 at src/code_generator/mod.rs:2655, 2820, 2854, 2890, 2908) now return null for any vector over MAX_LSH_DIMENSION = 16,384 (src/size_limits.rs:38). The intent doesn't call for this. The hyperplane allocation is bits * dimension with bits capped at 62, so it is at most 62 times the size of a vector that is already in memory. It can't request an amount the address space can't hold, which is the abort the intent is about. Meanwhile a legitimate embedding over 16,384 dimensions now silently loses its LSH bucket or probes and drops out of LSH joins without any error. Recommended fix: remove the dimension check. If the concern is the size of the hyperplane cache, deal with that separately, since this cap doesn't bound the number of cache entries anyway.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Audit repro: lsh_probes(0,4,4) returns [0,1,2,4]; num_probes=3e17 is refused with a validation error and the server keeps serving ✅ pass live Audit P0 repro artifact; pgrep showed server PID 1413287 alive afterwards
Adversarial: num_probes of 3e17 read from a stored fact (not a literal) yields only the 15 distinct probes for lsh_probes and 8 for lsh_multi_probe, without aborting ✅ pass live s2_transcript.txt
lsh_multi_probe literal 3e17 and lsh_probes one over the 39,774 bound are refused with errors that name the bound ✅ pass live s2_transcript.txt
hnsw_nearest on a 1,100-row index: k=3e17, ef_search=3e17, 10001 and 0 refused; ef_search=10000 returns 5 neighbours ✅ pass live s2_transcript.txt
.index create with ef_construction 10001 or ef_search 3e17 is refused with an index error naming the bound ✅ pass live s2_transcript.txt
top_k<1000001> and top_k_threshold<3e17> refused; top_k<1000000> over 3 rows returns 3 rows ✅ pass live s2_transcript.txt
User decision: a 20,000-dimension embedding still gets an LSH bucket and multi-probe results (not null) ✅ pass live s3_transcript.txt
Oversized replace (empty pattern) and 6-deep nested concat return null instead of allocating; normal replace still returns 'ayyc' ✅ pass live s3_transcript.txt
Fixed-seed fuzz of every size position (boundaries, u64::MAX, random) never takes the handler down ⏸️ untested no The prior payload did not establish a live result for this scenario: the fuzz ran only through cargo test --test size_limit_tests against an in-process Handler, not the running server binary. To dri…
  • target/debug/inputlayer-server --host 127.0.0.1 --port 27493 --data-dir &lt;tmp&gt; + inputlayer-client --server http://127.0.0.1:27493 s1.iql (audit repro: lsh_probes 4 vs 3e17)
  • inputlayer-client ... s2.iql: runtime num_probes from data, lsh_multi_probe literal, hnsw_nearest k/ef_search on a 1,100-row index, .index create ef_construction/ef_search, top_k / top_k_threshold over and at bound
  • inputlayer-client ... s3.iql: 20,000-dim lsh_bucket / lsh_multi_probe, oversized replace/concat return null, normal replace still works
  • pgrep after each script to confirm the server process survived
  • cargo test --test size_limit_tests (9 handler-level tests including the fixed-seed fuzz over every size position)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

jsam added 2 commits October 4, 2026 20:36
…ocation

One query with a huge client-supplied count, such as
lsh_probes(0, 4, 300000000000000000), made the engine ask the allocator for
that many bytes; the allocation failed and the process aborted, before the
per-query memory limit could act. hnsw_nearest's k and ef_search, top_k's K,
and an index's ef_construction were the same class, and hnsw_rs exits the
process outright for m over 256.

Each size is now checked twice. The parser refuses a literal over its bound
with a validation error that names the parameter and the bound (new
size_limits module): top_k and top_k_threshold K at most 1,000,000,
hnsw_nearest k and ef_search at most 10,000 (ef_search 0 is refused too),
lsh_probes and lsh_multi_probe num_probes at most 39,774 (every distinct
probe for 62 bits), and .index create ef_construction and ef_search at most
10,000. The evaluator bounds each allocation by what its input can produce,
so a size bound at run time is safe as well: probe vectors by the distinct
probe count, top-k heaps by the group size, the HNSW beam by the index size
(k * 4 and the widening loop saturate), and saved index definitions are
clamped when their graph is built. replace and concat return null rather
than build a string over 16 MiB, the LSH functions return null for vectors
over 16,384 dimensions (their cached hyperplanes are bits x dimension
floats), and a client timeout too far out for Instant no longer panics.
@jsam
jsam merged commit 8b574ef into main Oct 5, 2026
26 of 27 checks passed
@jsam
jsam deleted the fm/fix-size-clamps branch October 5, 2026 06:11
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.

1 participant