high

CVE

CVE-2026-71281

CWE

CWE-502

Affected Surface

  • Hugging Face `peft` versions `0.6.0` through `0.19.1` on PyPI per the published CVE metadata
  • The public `v0.20.0` tag still shows the same direct `torch.load()` call sites in `corda.py` and `loraga.py` at the time of writing, so defenders should verify upstream remediation status before treating `0.20.0` as fixed
  • ML training and inference environments that load LoRA-GA `cache_file` or CorDA `cache_file` / `covariance_file` from downloaded, shared, or multi-tenant storage
  • Internal AI platforms, notebooks, fine-tuning pipelines, and agent backends that exchange PEFT preprocessing artifacts between users, jobs, or workspaces

CVE-2026-71281 matters because the vulnerable boundary is small, easy to miss, and common in modern ML plumbing: peft accepts cache-like artifacts that are supposed to accelerate LoRA initialization, but the CorDA and LoRA-GA preprocessing paths load those artifacts with raw torch.load() calls instead of PEFT’s own safer helper that forces weights_only=True.

That means a hostile cache or covariance file is not “just bad model data.” In the vulnerable path, it is a pickle payload that executes during preprocessing on the same GPU host, notebook, or training worker that already has access to model weights, API tokens, datasets, and sometimes cloud credentials.

Affected package and version caveat

The published CVE metadata names the PyPI package and affected range as:

  • peft 0.6.0 through 0.19.1

What makes this disclosure worth closer technical review is that the public code still looks unresolved beyond that published boundary. At the time of writing:

  1. the v0.19.1 tag still contains the unsafe torch.load() calls
  2. the v0.20.0 tag still contains the same direct calls
  3. the current public main branch still shows the same pattern

That does not automatically rewrite the official affected range, but it does mean defenders should not assume “0.20.0 exists, therefore this is fixed.” The safer operational stance is to verify the exact code path you run and wait for an explicit upstream security fix note.

Where the unsafe deserialization happens

The CorDA preprocessing path loads both an eigens cache and a covariance cache directly from file paths controlled by configuration state:

if cache_file is not None and os.path.exists(cache_file) and os.path.getsize(cache_file) > 0:
    cache = torch.load(cache_file, map_location=get_model_device(model))

and later:

if covariance_file is not None and os.path.exists(covariance_file) and os.path.getsize(covariance_file) > 0:
    all_covariance_matrix = torch.load(covariance_file, map_location=get_model_device(model))

The LoRA-GA preprocessing path does the same thing for its gradient cache:

if cache_file is not None and os.path.exists(cache_file) and os.path.getsize(cache_file) > 0:
    cache = torch.load(cache_file, map_location=get_model_device(model))

The CVE record is accurate about the boundary failure: these calls bypass PEFT’s own safer load wrapper that already exists elsewhere in the project.

The project already had a safer pattern

In save_and_load.py, PEFT ships a helper that pins the safer policy explicitly:

def torch_load(*args, weights_only=True, **kwargs):
    """Call torch.load and handle weights_only.

    Defaults to weights_only=True to anticipate upcoming switch on the PyTorch side.
    """
    return torch.load(*args, weights_only=weights_only, **kwargs)

That contrast is the technical heart of the bug:

safe path elsewhere in PEFT:
torch_load(..., weights_only=True)

unsafe CorDA / LoRA-GA path:
torch.load(...)

This is not a case where the project had no concept of safer loading. It had the control, but the control was not applied to the cache-loading paths that mattered here.

The older PR that introduced the wrapper is also useful context. PEFT maintainers had already documented why relying on implicit torch.load() behavior is brittle: the security-sensitive choice should be explicit in project code, not inherited from whatever default a caller’s PyTorch runtime happens to use.

Why this becomes code execution

The dangerous part is not specific to LoRA math. It is the object-loading primitive.

When torch.load() is called without weights_only=True, common PyTorch deployments still permit full pickle deserialization. A malicious cache artifact can therefore smuggle an object with a hostile __reduce__() path or equivalent pickle gadget chain.

Reduced to its essential flow, the vulnerable path looks like this:

attacker-controlled cache_file / covariance_file
-> preprocess_loraga() or preprocess_corda()
-> file exists, so preprocessing skips recomputation
-> torch.load(file, ...)
-> pickle-backed object graph is deserialized
-> attacker-controlled code runs inside the training or inference process

That matters because these functions are specifically designed to make expensive preprocessing reusable. In real deployments, “reusable” often means:

  • a shared object store bucket
  • a team-wide NFS path
  • notebook outputs copied between users
  • experiment artifacts downloaded from prior runs
  • platform-managed caches materialized into per-job work directories

Once the system treats those files as trusted acceleration artifacts instead of as executable content, the security boundary is gone.

Why AppSec teams should care even if this is “just ML tooling”

The obvious victims are ML engineers running PEFT fine-tuning workflows. The higher-value targets are the systems behind them:

  • managed training jobs with cloud-instance roles
  • notebook servers that can reach internal data stores
  • agent backends that let users upload or reuse training artifacts
  • CI or batch systems that evaluate adapters as part of release or benchmarking workflows

In those environments, arbitrary code execution during model preprocessing is not a toy impact. The compromised process may already have access to:

  • Hugging Face tokens
  • cloud credentials
  • internal datasets
  • model checkpoints
  • experiment metadata and prompts
  • service-to-service secrets mounted for downstream evaluation steps

So while the bug lives in a PyPI package, the blast radius is often broader than one Python environment.

The version ambiguity is part of the story

One reason this issue is easy to mis-triage is that the public record is not yet perfectly aligned:

  • the CVE metadata currently marks versions through 0.19.1 as affected
  • PyPI’s latest version is 0.20.0
  • direct source review of v0.20.0 still shows the same raw torch.load() calls in the same functions

That mismatch means defenders should answer a code question, not just a package-version question:

Does the exact PEFT code we deploy still deserialize LoRA-GA or CorDA cache files
through raw torch.load() instead of an explicit safe-only wrapper?

If yes, the exposure model has not meaningfully changed, even if the published advisory has not yet been updated to name that tag.

Scoping and detection

Start with dependency inventory:

python3 -m pip show peft torch
rg -n '(^|[ =])peft([<>= ]|$)' requirements*.txt pyproject.toml poetry.lock Pipfile.lock

Then locate the vulnerable usage patterns in your code:

rg -n 'preprocess_loraga|preprocess_corda|lora_ga_config|corda_config' .
rg -n 'cache_file|covariance_file' .

The scoping question is not merely “do we import peft?” It is:

Can a user, tenant, notebook author, job definition, or downloaded artifact influence
the LoRA-GA cache file or CorDA cache/covariance file that PEFT will load?

If the answer is yes, review where those files come from:

  • object-store downloads
  • shared scratch volumes
  • previous-run artifacts
  • public examples or tutorial bundles
  • cross-tenant experiment reuse

On training hosts, inspect recent jobs for suspicious .pt, .pth, .bin, or cache-like files referenced in LoRA-GA or CorDA configuration. Also review notebook histories, orchestration parameters, and artifact manifests for paths that skipped recomputation because the file already existed.

Remediation

If you can avoid these initialization modes temporarily, that is the lowest-risk choice. Otherwise:

  1. do not load LoRA-GA or CorDA cache artifacts from untrusted or shared sources
  2. regenerate caches inside the trusted job that will use them
  3. isolate training and evaluation workers from high-value credentials
  4. prefer artifact formats and workflows that do not rely on pickle-backed torch.load() for cross-user interchange
  5. watch for an explicit upstream fix rather than assuming the newest tag is safe

For teams that vendor or hot-patch dependencies internally, the minimal code-level repair is conceptually simple:

-cache = torch.load(cache_file, map_location=get_model_device(model))
+cache = torch.load(cache_file, map_location=get_model_device(model), weights_only=True)

or, even better, route these paths through the same helper PEFT already uses elsewhere:

from peft.utils.save_and_load import torch_load

cache = torch_load(cache_file, map_location=get_model_device(model))

That patch does not solve every model-loading risk in the broader PyTorch ecosystem, but it restores the specific boundary PEFT already intended to enforce.

Incident-response guidance

If a less-trusted file may have been loaded through these paths:

  1. treat the host as potentially compromised
  2. rotate secrets reachable from the process from a different trusted machine
  3. review outbound network activity and shell history for the affected job or notebook
  4. examine downloaded artifacts and serialized cache files tied to the run
  5. preserve the suspicious cache file for offline analysis rather than reloading it in-place

The bigger lesson is that ML artifact caches are now part of the application-security boundary. CVE-2026-71281 is not about exotic tensor math. It is about a familiar software-security failure showing up inside AI tooling:

the project knew the safe primitive,
but one performance-oriented code path still loaded attacker-shaped data with the unsafe one

That is exactly the kind of bug AppSec teams should expect more of as model-training and model-adaptation workflows keep turning serialized data files into privileged execution surfaces.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References