huggingface / huggingface/candle

Deterministic panic (DoS) via `Long1` opcode in candle-core's pickle reader

Open Beginner friendly
#3,619 1 comment 1 reaction 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.