huggingface / huggingface/candle
Deterministic panic (DoS) via `Long1` opcode in candle-core's pickle reader
- Dominant language
- Rust
- Stars
- 21k
- Forks
- 1.8k
- Avg merge
- 16h 42m
- Merged PRs (30d)
- 25
Description
## Summary
candle-core's pickle VM decodes the `Long1` opcode by reading an attacker-controlled byte count
`n_bytes` (0–255) and shifting each subsequent byte into a fixed-width `i64`. There is no
`n_bytes > 8` guard, so when `n_bytes > 8` the loop reaches `i = 8` and computes `(byte as i64) << 64`.
Rust's shift-left operator panics unconditionally when the shift amount is >= the target bit width
("attempt to shift left with overflow"). The result is a deterministic panic / DoS crash that aborts
the process on a crafted pickle stream embedded in a `.pth` model file. The panic propagates
regardless of caller error handling (including `std::panic::catch_unwind`).
## Affected component
- Repository: `huggingface/candle` (`candle-core` crate)
- File: `candle-core/src/pickle.rs:609` — the `Long1` opcode handler:
`v |= (r.read_u8()? as i64) << (i * 8)` (panics when `i == 8`)
- Reachable from `candle_core::pickle::Stack::read_loop()`, exercised by
`read_pth_tensor_info()`, `PthTensors::new()`, and `read_all()` on untrusted `.pth` files.
## Reproduction
PoC: `findings/candle/poc-072-pickle-long1.bin` (11 bytes).
```
8a 09 00 00 00 00 00 00 00 00 00
```
Byte 0 (`0x8a`) is the `Long1` opcode; byte 1 (`0x09`) sets `n_bytes = 9` (> 8); the remaining 9
bytes are consumed by the loop, with `i = 8` triggering `(0 as i64) << 64`.
The PoC can also be regenerated:
```python
poc = bytes([0x8a, 0x09, 0,0,0,0, 0,0,0,0, 0]) # Long1, n_bytes=9, 9 payload bytes
open('poc-072.bin', 'wb').write(poc)
```
Load via any candle path that calls `Stack::read_loop()` (e.g. `PthTensors::new()` /
`read_pth_tensor_info()`).
Observed panic:
```
thread '' panicked at candle-core/src/pickle.rs:609:26:
attempt to shift left with overflow
```
## Root cause
`n_bytes` is read directly from the stream and used as a shift count with no upper bound. Python's
`Long1` format supports arbitrary-precision integers, but candle decodes into a fixed `i64` and does
not reject `n_bytes > 8`, so the shift amount reaches 64 and the operation panics.
```rust
// pickle.rs:601-610 (Long1 handler)
OpCode::Long1 => {
let n_bytes = r.read_u8()?; // attacker-controlled, 0-255
let mut v = 0;
for i in 0..n_bytes {
v |= (r.read_u8()? as i64) << (i * 8); // line 609 — panics when i >= 8
}
self.push(Object::Long(v))
}
```
## Suggested fix
Validate `n_bytes` before the loop:
```rust
OpCode::Long1 => {
let n_bytes = r.read_u8()?;
if n_bytes > 8 {
candle_core::bail!("Long1: n_bytes {n_bytes} exceeds i64 capacity (8)");
}
let mut v = 0i64;
for i in 0..n_bytes {
v |= (r.read_u8()? as i64) << (i * 8);
}
self.push(Object::Long(v))
}
```
Alternatively, decode into a `Vec` and convert with overflow handling to preserve support for
large Python integers stored as `Object::Long`.
## Note on sibling findings
The earlier candle GGUF findings in this series (CRUCIBLE-2026-070 / 071 / 079 / 080) are now FIXED
upstream — candle hardened its GGUF parser (GGUF_MAX_STRING_LENGTH / _ARRAY_ELEMENTS / _TENSOR_DIMS
caps plus remaining-byte checks). This pickle `Long1` path is the one that remains live. This issue
covers only the pickle path.
## Revalidation
Source-level revalidation on 2026-06-14 against candle HEAD `65ecb58`: `pickle.rs:609` still performs
`v |= (r.read_u8()? as i64) << (i * 8)` inside the `Long1` loop with no `n_bytes > 8` check — the
shift-left panic is intact.
Status: advisory drafted; re-routing to public issue + VulDB; revalidated 2026-06-14 source-level vs
candle HEAD `65ecb58`.
## Proof-of-concept files (base64)
Decode with `base64 -d > file`.
**poc-072-pickle-long1.bin** (11 bytes):
```
igkAAAAAAAAAAAA=
```
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in candle-core/src/pickle.rs at the Long1 handler around line 609, then trace calls through Stack::read_loop() and PthTensors::new() or read_pth_tensor_info(). Exercise the supplied poc-072-pickle-long1.bin and verify that an n_bytes value above 8 returns an error without triggering a shift-overflow panic.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100