Boost your digital potential

Intlix Intelligence Layer © 2026

RESEARCHIdentity_/from-memory-corruption-to-rce

From Memory Corruption to RCE: Inside a PyTorch Model-Loading Exploit Chain

Sep 5, 2026
INTEL_BROADCAST
From Memory Corruption to RCE: Inside a PyTorch Model-Loading Exploit Chain

From Memory Corruption to RCE: Inside a PyTorch Model-Loading Exploit Chain

How a crafted PyTorch model can move from a model-loading bug to memory corruption—and potentially remote code execution in an AI application.

Introduction

Machine-learning models are, at the end of the day, files.

They are downloaded from Hugging Face, shared between teams, stored in artifact repositories, and loaded by application code that often assumes the model is simply data. But what happens when that assumption is wrong?

What if a model file is not just a collection of weights, but a carefully crafted input capable of triggering memory corruption?

That question sits at the heart of security research presented by independent researchers Lulay and Georgian. Their work, initially presented at Black Hat USA and later extended with newly disclosed memory-corruption findings, traces an attack chain through PyTorch's model-loading infrastructure.

The chain starts with a weakness in PyTorch's supposedly safer model-loading path, moves through a heap out-of-bounds primitive in tensor internals, and ultimately demonstrates how those primitives can be used to pursue remote code execution in a real-world AI application.

This post walks through that research, the underlying bug, the exploitation challenges, and the defensive lessons for teams deploying AI systems.

Why Model Loading Deserves Security Scrutiny

A typical machine-learning lifecycle looks something like this:

Define scope → Prepare data → Select model → Train / RLHF / Evaluate → Optimize & deploy → Monitor & build applications

The model sits at the center of that entire pipeline. Everything downstream depends on it.

At the same time, modern ML workflows make it remarkably easy to consume models from external sources. Models can be downloaded from public hubs, shared across organizations, or automatically retrieved by production infrastructure.

That creates an important security boundary that is easy to overlook: loading a model is itself an attack surface.

Historically, model-loading vulnerabilities have generally fallen into two broad categories.

1. Deserialization vulnerabilities

These are the familiar problems associated with formats such as Python's pickle. If an application deserializes attacker-controlled data, arbitrary code execution can sometimes occur during the loading process.

Similar problems have also appeared in other ML ecosystems, including bypasses involving Keras's "safe mode."

2. Memory-safety vulnerabilities

The second category involves bugs in the low-level code responsible for parsing and manipulating model data.

Examples include integer overflows and out-of-bounds memory accesses in model-loading or tensor-processing code.

Deserialization vulnerabilities have received considerable attention. Memory-safety problems hiding inside model formats and tensor operations, however, have received considerably less public scrutiny.

That gap became the focus of this research.

Why PyTorch?

The researchers chose PyTorch as their target for a straightforward reason: it is one of the most widely used deep-learning frameworks, and its model formats are deeply embedded throughout the ML ecosystem.

PyTorch models are commonly serialized using mechanisms such as torch.save and loaded with torch.load.

That makes the security properties of the loading process particularly important.

The weights_only Story

In PyTorch's early days, torch.load relied heavily on Python's pickle machinery. As a result, loading an untrusted model could lead to arbitrary code execution.

PyTorch introduced the weights_only=True option as a safer alternative, designed to restrict loading to tensor data rather than arbitrary executable objects.

That was an important security improvement—but the researchers found that the protection had an unexpected escape route.

When torch.load(..., weights_only=True) encounters a file identified as TorchScript, it can hand the file over to torch.jit.load.

TorchScript has a substantially broader operator surface than simple tensor deserialization, including operations capable of interacting with the filesystem.

In other words, the security boundary around weights_only=True was not as strong as its name might suggest.

The researchers had previously presented this logic/deserialization issue at Black Hat USA. The work described here continues from that discovery, focusing instead on the memory-corruption vulnerabilities uncovered during the investigation.

Understanding PyTorch's Tensor Memory Model

To understand the vulnerability, it helps to have a basic mental model of how PyTorch tensors are represented internally.

At a high level, creating a tensor involves several stages:

  1. Parsing the supplied arguments

  2. Checking the input

  3. Inferring metadata such as shape, dtype, and device

  4. Allocating storage

  5. Filling the allocated buffer

  6. Normalizing the resulting tensor

  7. Returning the tensor to the caller

Two internal C++ structures are particularly important: TensorImpl and StorageImpl.

TensorImpl

TensorImpl acts as the primary internal representation of a tensor.

Among other things, it contains:

  • Reference-management information

  • A pointer to the underlying Storage

  • Tensor sizes and strides

  • storage_offset

  • The number of elements

  • Dtype and device information

The critical field for this research is storage_offset.

StorageImpl

StorageImpl represents the underlying data container. It contains information such as:

  • Reference-management data

  • A destructor function pointer

  • data_ptr, pointing to the actual data buffer

  • Device information

  • The size of the storage in bytes

The relationship between these structures is crucial.

A tensor's storage_offset determines where reading begins relative to the data_ptr stored in its underlying storage.

This is what allows PyTorch to create zero-copy tensor views: multiple tensors can reference the same storage while interpreting different regions of that storage.

That flexibility, however, means that incorrect offset validation can become extremely dangerous.

Hunting for a Memory-Corruption Primitive

The researchers approached the problem systematically.

They looked for operators whose parameters resembled raw memory operations—things such as offsets, indices, and strides. These are natural places to look for missing or incorrect bounds checks.

One particularly interesting candidate was:

torch.as_strided

The function allows callers to construct a tensor view while directly specifying its size, stride, and storage offset.Under normal circumstances, PyTorch correctly rejects obviously invalid offsets.

For example, asking for a six-element view starting at an offset that would require a seventh element results in an out-of-bounds error. Negative offsets are also rejected. But extremely large offsets produced a surprising result.

An offset such as 2**62 behaved as though the offset were zero.

More interestingly, offsets near that value produced values that appeared to come from outside the tensor's legitimate storage.

One observed value was 0x41—a byte consistent with glibc heap metadata. That was a strong indication that the tensor was reading outside its allocated buffer.

Further testing confirmed that the behavior was not a one-off anomaly. The researchers had found a heap out-of-bounds read/write primitive.

The Root Cause: Integer Overflow

The underlying issue was an integer overflow in the bounds-checking calculation. Conceptually, the vulnerable logic looked like this:

bool check_in_bounds_for_storage(
    int64_t storage_size_bytes,
    int64_t storage_offset,
    int64_t new_size_bytes) {

    int64_t offset_bytes = storage_offset * element_size;

    return (new_size_bytes + offset_bytes) <= storage_size_bytes
           && storage_offset >= 0;
}

The problem is the multiplication. Consider a six-element int64 tensor. Each element requires eight bytes, so the legitimate storage size is:

6 × 8 = 48 bytes

Now consider an attacker-controlled offset of: 2⁶² − 1

That value is positive and therefore passes the explicit storage_offset >= 0 check. But multiplying it by eight exceeds the range of a signed 64-bit integer. The result wraps around to a negative value. The subsequent bounds calculation therefore operates on a completely incorrect number.

Instead of rejecting the enormous offset, the check can effectively conclude that the requested memory access falls inside the legitimate allocation. The oversized offset is then stored in TensorImpl.

From that point onward, tensor accesses are calculated relative to an address that is nowhere near the tensor's actual data. The result is effectively a heap-underflow primitive.

Why This Matters

Once an attacker has control over the effective storage offset, two particularly useful capabilities emerge:

  • Out-of-bounds reads, which can expose heap and library addresses and help defeat ASLR.

  • Out-of-bounds writes, which can potentially corrupt adjacent heap objects, including control-flow-relevant data.

This transforms what initially looks like a numerical validation bug into a potentially powerful memory-corruption primitive.

Turning the Primitive Into an Exploit

Finding the bug was only the beginning. Turning an out-of-bounds primitive into a reliable exploit introduced several practical challenges.

1. Heap Spraying and Dead-Code Elimination

The researchers used large numbers of tensor-related objects to influence the heap layout.

But TorchScript's JIT compiler introduced an unexpected complication: dead-code elimination. Objects that were created but never subsequently used could be optimized away from the serialized model.

The solution was straightforward. The researchers added operations that made the sprayed objects observably used—for example, accessing or printing their shapes. That prevented the optimizer from deciding that the objects were unnecessary.

2. Finding the Right Object

The next challenge was identifying the relevant StorageImpl object within the sprayed heap.

The researchers searched memory for structures exhibiting characteristics consistent with a live StorageImpl, including:

  • Expected allocation sizes

  • Plausible data_ptr values

  • Live reference-count patterns

  • Pointer values consistent with loaded shared libraries

Once a candidate was found, the researchers could verify it by modifying its data_ptr and checking whether the corresponding sprayed data changed. This provided a write-and-verify oracle for identifying the correct object.

3. Leaking Addresses

Memory corruption alone is rarely enough on a modern Linux system.

ASLR randomizes the locations of important libraries and objects, so an attacker generally needs an information leak before reliably targeting specific addresses.

The researchers found useful pointers inside StorageImpl, including pointers associated with the C++ object infrastructure and libc.

Those leaks could be used to calculate the runtime addresses of relevant libraries and, ultimately, functions such as system().

4. Control-Flow Hijacking

The final stage was a classic memory-corruption technique adapted to PyTorch's C++ object model.

With control over a relevant function pointer and a way to trigger the associated object's destruction, the researchers demonstrated a path toward redirecting execution.

The conceptual chain is:

memory corruption → address disclosure → function-pointer overwrite → destructor trigger → controlled execution

That is a significant escalation from a malformed tensor offset.

From a Library Bug to an AI Application

A vulnerability in a library is interesting. A vulnerability reachable through a deployed application is considerably more concerning.

The researchers therefore looked for AI applications capable of loading user-supplied TorchScript models. One notable target was OpenSearch, an open-source search and observability platform with integrated ML model functionality.

The resulting attack path illustrates an important point: exploitability depends not only on the underlying framework, but also on how an application exposes model loading.

The Application-Level Attack Path

In the researchers' test environment, the attack involved several stages. First, OpenSearch was deployed using Docker Compose.

The researchers then configured the application's model-management functionality and enabled the ability to register a model through a URL. This setting was disabled by default.

Once a remotely hosted model could be registered, the researchers supplied a malicious model together with its expected SHA-256 hash and proceeded through the model deployment and execution pipeline.

At that point, the model itself became the delivery mechanism.

Error Messages as an Oracle

One particularly interesting part of the research was the use of error messages as a side channel.

The researchers could deliberately trigger errors and observe information returned by the model-loading pipeline. That turned seemingly harmless error handling into an information-disclosure primitive.

For example, they demonstrated arbitrary file-read behavior by abusing torch.from_file.

The process could first be used to infer the length of a target file through an error condition and then request the appropriate length to retrieve the file's contents through the resulting error message.

The researchers also investigated arbitrary file writes.

However, torch.save writes additional PyTorch metadata alongside the payload. This "dirty data" makes some targets unsuitable because appending unexpected bytes would corrupt the file.

Files that tolerate additional content are therefore more practical targets than highly structured configuration files.

The Seccomp Wall

At this point, the research had demonstrated a significant memory-corruption chain.

But there was still an important obstacle: sandboxing.

The researchers successfully resolved libc and the address of system(). Debugging confirmed that execution reached the function. Yet a shell did not appear.

The reason was the container's seccomp policy. The filter blocked execve and related system calls such as fork, preventing the straightforward transition from arbitrary code execution to a conventional shell.

This is an important security lesson. A successful memory-corruption exploit does not automatically mean unrestricted host compromise.

Defense-in-depth mechanisms such as seccomp can meaningfully constrain what an attacker can do after gaining control of application execution.

The researchers explored alternative approaches using system calls that remained available, including directory operations and raw write operations for data exfiltration.

In other words, the sandbox forced the exploitation problem to become a different one: rather than simply spawning a shell, the attacker would need to work within the capabilities that the sandbox still permitted.

Scope and Real-World Impact

There are important limitations to the demonstrated attack chain.

First, the specific OpenSearch configuration required security-sensitive settings to be enabled. Model registration through a remote URL was disabled by default in the tested environment.

Second, AWS's managed OpenSearch Service does not expose that particular configuration option, meaning the demonstrated chain does not directly translate to AWS-hosted deployments.

The OpenSearch security team also did not classify the issue as a new vulnerability. Their reasoning included the fact that PyTorch already documents the risks associated with loading untrusted TorchScript models, the relevant OpenSearch setting is disabled by default, and operating-system-level sandboxing is an expected mitigation.

Nevertheless, the research highlights a broader class of risk.

Any AI application that loads attacker-controlled TorchScript models may inherit the underlying PyTorch attack surface.

That includes inference-serving systems and other applications built on top of PyTorch.

What Defenders Should Do

The most important lesson is simple: Treat machine-learning models as potentially executable input—not as harmless data. For teams building AI systems, several defensive measures stand out.

Don't Load Untrusted Models

A model downloaded from an external source should be treated with the same caution as other untrusted artifacts. The fact that a file has a .pt extension does not make it safe.

Restrict Model Registration

Applications should tightly control who can:

  • Register models

  • Upload models

  • Point model loaders at remote URLs

  • Change model-loading configuration

  • Deploy models into production inference environments

Security-sensitive model-management features should not be casually exposed to ordinary application users.

Sandbox Model Loading

Sandboxing is one of the strongest lessons from this research.

The seccomp restrictions in the tested environment prevented the researchers' initial route from turning successful memory corruption into a straightforward shell.

Containers, seccomp profiles, dedicated low-privilege nodes, and isolated model-loading workers can all reduce the impact of a successful exploit.

Keep the ML Stack Patched

AI applications depend on large stacks of native and high-level dependencies. Frameworks such as PyTorch should be kept up to date, and security guidance should be followed closely. In particular, teams should not assume that a flag named weights_only makes arbitrary model loading universally safe. The exact model format and loading path still matter.

Closing Thoughts

For years, model security has been dominated by one familiar question:

Can loading this model execute Python code?

That remains an important question—but it is no longer the only one.

As well-known deserialization vulnerabilities become better understood and straightforward logic flaws are fixed, attackers have an incentive to move deeper into the stack.

That means looking at the native C++ code underneath the model-loading APIs, tensor operations, memory management, and other performance-critical components that often receive far less security scrutiny.

The research described here demonstrates why that matters. A single crafted model can potentially move through multiple layers of an AI application:

untrusted model → model loader → tensor operation → integer overflow → heap corruption → memory disclosure → control-flow manipulation → code execution

Even when sandboxing prevents the final step from becoming an unrestricted shell, the underlying primitive can still provide powerful capabilities. The broader lesson for AI developers is therefore straightforward:

A model file is an input. Treat it like one.

Security boundaries around model loading should be explicit, permissions should be restrictive, dependencies should be patched, and model-processing workloads should be isolated wherever possible.

As AI systems become increasingly dependent on third-party and remotely supplied models, model loading deserves to be treated as a first-class security boundary—not merely a convenience feature.