Attacks
This page reports the attacks in tools/attacks/ and how to reproduce them. Unless
stated otherwise, every number was measured on Qwen3-8B. The attacks need no key: they use
only what the cloud holds (see Exposure) and public material.
The scripts import kllm and run from a checkout of the repository with the seal
extra installed. tools/attacks/weights.py reads a sharded safetensors checkpoint (with
model.safetensors.index.json) and runs on a GPU by default (--device cuda).
Weights of a public model, or of a fine-tune of one
tools/attacks/weights.py seals a simulated fine-tune (base + eps·std·noise per tensor)
with a fresh random key and attacks it with only the public base and the sealed GPU weights.
At eps = 0 the sealed model is an unmodified copy of the public model. The GPU holds the
weights unlocked, so the attack applies to every model that runs, whether or not its files
on disk are locked; what locking changes for the files is described in
Locked weights.
Under orth
Second moments cancel the vocabulary permutation: for the LM head, W’ᵀW’ = Qᵀ(AᵀA)Q, so matching eigenvectors gives Q up to signs, which third moments fix. Nearest-neighbor matching and orthogonal Procrustes then refine Q and Π. With Q recovered, the other transforms reduce to nearest-neighbor matching.
eps |
Q relative error |
Π recovered |
MLP permutation recovered |
Error of recovered weights |
Error of using the base |
|---|---|---|---|---|---|
0.003 |
0.044 |
98.5% |
100% |
0.013 |
0.004 |
0.01 |
0.032 |
98.6% |
100% |
0.014 |
0.014 |
0.03 |
0.013 |
98.5% |
99.99% |
0.013 |
0.042 |
0.05 |
1.40 |
0.1% |
0% |
(failed) |
0.070 |
Under perm
The attack is easier. Each column of the LM head keeps its values, only reordered over the vocabulary, so the sorted magnitudes of a column identify it, and third moments give its sign.
eps |
Q relative error |
Π recovered |
MLP permutation recovered |
Error of recovered weights |
Error of using the base |
|---|---|---|---|---|---|
0 |
0.0000 |
98.5% |
100% |
0.0000 |
0 |
0.01 |
0.0028 |
98.6% |
100% |
0.0028 |
0.0100 |
0.03 |
0.0083 |
98.5% |
100% |
0.0083 |
0.0300 |
0.05 |
0.0139 |
98.5% |
100% |
0.0138 |
0.0499 |
0.1 |
0.0277 |
98.5% |
100% |
0.0277 |
0.0995 |
Interpretation
The 1.5% of Π not recovered are near-duplicate rows (unused tokens). Real fine-tunes change
weights by well under 3%. The static transforms therefore do not protect a fine-tune of a
public model from a cloud that reads GPU memory, under either transform; under perm,
not even a large fine-tune. At eps = 0.05 the spectral start for orth fails; a better
start would likely succeed.
The perm row at eps = 0 is a sealed copy of the unmodified public model: Q is recovered
exactly, the MLP permutation completely and Π to 98.5%, the remainder being the unused
near-duplicate tokens. Under orth the smallest measured change, 0.3% of the weight
standard deviation, gives the same Π recovery; an unmodified model was not run separately
under orth. A sealed copy of a public model therefore keeps neither its transforms
nor Π secret from any party that holds the public weights and the unlocked sealed
weights, and such a party can map the
sealed model’s token ids to text without the key. The key itself is not recovered, since it
only seeds the transforms through HKDF, and trusted.kllm stays encrypted; what the blob
protects, Π, is read from the weights instead. This is the position of every sealed copy of
a public checkpoint once its weights are unlocked to run, including the catalog models
(The catalog of official sealed models), whatever key seals them.
For weights with no public relative there is no reference to match against. Under
orth, recovering Q then amounts to blind source separation; no attack is known for that
case, and no proof of security exists either. Under perm, per-channel statistics
survive, which leaves more structure to work with.
The same holds for MiniMax H3: the FL2VA weights are public, so a sealed copy does not
protect the weights, and Π of its text encoder is expected to fall to the same attack
(not measured on H3); what the seal still withholds from a reader of logs and relayed
payloads is the user’s text. It also holds for FP8 checkpoints,
which are sealed under perm only, and for mixture-of-experts models sealed under
perm: the column-profile attack on the LM head applies unchanged. Experts are expected
to be matched to those of the base by the same kind of statistics as MLP channels, while
the block structure of FP8 permutations leaves fewer orders to search. Neither expectation
has been measured on these models.
Note
Scope of the measurement: the script attacks the LM head (for Q and Π) and the
gate_proj of one layer (--layer, default 10, for the MLP permutation). It uses
the transform code of kllm seal, not the command itself. The statement that the
other transforms reduce to nearest-neighbor matching once Q is known is not measured
for attention heads, RoPE turns or the V/O transforms.
To reproduce the two tables:
$ python tools/attacks/weights.py --base ~/models/Qwen3-8B --eps 0.003 0.01 0.03 0.05
$ python tools/attacks/weights.py --base ~/models/Qwen3-8B --transform perm \
--eps 0 0.01 0.03 0.05 0.1
Further options: --rounds (refinement rounds, default 6), --sample (rows used per
round, default 32768) and --device (default cuda).
User tokens
tools/attacks/tokens.py used a 150k-token corpus written by Qwen3-8B on 24 topics: half
served as the attacker’s reference, half as the victim’s traffic.
Token ids. The GPU sees a substitution cipher over the vocabulary. Plain unigram frequency matching decodes 23.8% of victim tokens; n-gram or language-model attacks perform better.
Activations. Q is an isometry, so the GPU can cluster activations. The nearest neighbor of a token’s layer-1 input is the same token 99.4% of the time (chance 0.6%), and the rate stays above 50% at every layer of the model. A signed permutation is an isometry as well, so the same holds under
perm. Under either transform the GPU can build a token codebook and is back to the substitution cipher.Logits. The argmax index of every step is Π⁻¹(next token), another substitution cipher, on output tokens.
Layer input |
1 |
2 |
4 |
8 |
12 |
16 |
20 |
24 |
28 |
32 |
36 (final) |
|---|---|---|---|---|---|---|---|---|---|---|---|
Same-token 1-NN rate |
99.4% |
97.1% |
95.1% |
87.7% |
75.9% |
69.1% |
63.2% |
58.8% |
55.2% |
51.9% |
53.5% |
Note
Scope of the measurement: the substitution cipher is simulated by applying Π⁻¹ from a fresh random key to the victim’s token ids. The nearest-neighbor rates are measured on activations of the plain model; they carry over to the sealed model because both transforms are isometries of the residual stream.
To reproduce, write a corpus with a plain model server that exposes
/v1/chat/completions, then run the attack. --all-layers adds the per-layer
nearest-neighbor measurement and runs the full model on a GPU:
$ kllm serve ~/models/Qwen3-8B --port 30001
$ python tools/attacks/tokens.py --generate http://127.0.0.1:30001 --corpus corpus.txt
$ python tools/attacks/tokens.py --base ~/models/Qwen3-8B --corpus corpus.txt --all-layers
Corpus options: --texts (default 400) and --max-tokens (default 400). Measurement
options: --seq (default 512) and --nn-tokens (default 8192).
MiniMax H3 outputs
The H3 VAE decoders run on the GPU, so the generated video and audio are in GPU memory in the clear. The transforms hide the prompt (permuted token ids, text-encoder activations in the secret basis) and the latents during denoising (a fixed permutation of channels, which statistics would undo).
A stolen checkpoint, or a wrong key
The checkpoint on disk is all that a thief of the deployer’s storage obtains. Without the
key the tokenizer cannot be decrypted (trusted.kllm is AES-256-GCM encrypted under a
key derived from the model key), and kllm serve refuses a key with the wrong
fingerprint. Run with the public tokenizer instead, models sealed without locking produce
nothing useful: Qwen3 emits unrelated tokens in random scripts, and H3, given the same
prompt and seed, generates an unrelated video (frame PSNR 10.5 dB against plain, audio
spectrogram SNR −11.4 dB). Locked weights compute nothing useful with any tokenizer.
Locked weights
By default the weights on disk are locked: encrypted in place under a key stream derived from the model key (Locked weights). Without the key, each locked tensor is, up to the security of AES-256, uniformly random codes in the range of finite values up to its largest power of two. A thief who also holds the public model from which the sealed weights derive therefore has nothing to match them against, and the attack in Weights of a public model, or of a fine-tune of one does not apply to the locked files. What stays in the clear is the layout (tensor names, shapes and dtypes, and the configuration) and each tensor’s largest binary exponent.
This holds for the files only. Wherever the model is unlocked to run, on a server with the
key, in its cache of unlocked weights or in the cloud’s copy for client-keyed serving, the
weights are in the clear, and a party that holds them and the public base is in the case of
Weights of a public model, or of a fine-tune of one. A thief of a checkpoint sealed with --no-lock, or by a
kLLM version without locking, is in that case as well: that thief recovers the transforms
and Π from the sealed weights and needs neither the key nor the encrypted tokenizer.
Client-keyed Z-Image latents
In client-keyed mode the Z-Image latents in the server’s GPU memory show the image; a principal component analysis without the key recovers a clear thumbnail. The figure and the reasoning are in Not protected in client-keyed mode.
See also
Exposure for the material each attack uses.
Claims for the claims that follow from the measurements.
Choosing a transform for how the results bear on the choice of transform.