Skip to content

Harden the FFI parser cache against code execution (restrict unpickling) - #562

Open
Nexory wants to merge 2 commits into
angr:masterfrom
Nexory:harden/ffi-cache-restricted-unpickler
Open

Harden the FFI parser cache against code execution (restrict unpickling)#562
Nexory wants to merge 2 commits into
angr:masterfrom
Nexory:harden/ffi-cache-restricted-unpickler

Conversation

@Nexory

@Nexory Nexory commented Aug 6, 2026

Copy link
Copy Markdown

Problem

import pyvex (and therefore import angr) reads a cffi FFI parser cache from a world-predictable path in the shared temp directory and deserializes it with a bare pickle.loads (pyvex/native.py):

cache_location = os.path.join(tempfile.gettempdir(), f"pyvex_ffi_parser_cache.{username}.{hash_}")
if os.path.isfile(cache_location):
    with open(cache_location, "rb") as f:
        cache = pickle.loads(f.read())

The path is gettempdir()/pyvex_ffi_parser_cache.<getuser()>.<md5(ffi_str)>: the username is public and the hash is a fixed per-version constant. On a shared multi-user host a co-tenant can plant a poisoned cache file, and because pickle.loads executes arbitrary reduce callables, the next import pyvex runs attacker code as the victim (CWE-502 / CWE-377).

This is local, defense-in-depth hardening rather than a remote vulnerability, and it is the same deserialization-cache class recently hardened in keras and pdfminer.six.

Fix

The cache legitimately contains only cffi.model type classes, so a bare unpickle is unnecessary. This replaces pickle.loads with a RestrictedUnpickler whose find_class allows only cffi.model classes and rejects everything else, so a poisoned cache raises UnpicklingError instead of executing. No behaviour change on a legitimate cache.

Test

tests/test_ffi_cache_filter.py: an arbitrary-callable gadget is rejected; a cffi.model helper function is also rejected (the allow-list is class-only, so it cannot be used as a gadget); and a legitimate cffi.model cache still loads. The full test suite passes (67).

Nexory and others added 2 commits August 6, 2026 17:18
import pyvex reads a cffi FFI parser cache from a world-predictable path in the
shared temp directory (gettempdir()/pyvex_ffi_parser_cache.<user>.<hash>, where
the username is public and the hash is a fixed per-version constant) and
deserializes it with a bare pickle.loads. On a shared multi-user host a co-tenant
can plant a poisoned cache, and pickle.loads then executes arbitrary code as the
victim on the next import (CWE-502 / CWE-377). This is local, defense-in-depth
hardening, the same class recently addressed in keras and pdfminer.six.

The cache legitimately contains only cffi.model type classes, so this replaces
pickle.loads with a RestrictedUnpickler that permits only those classes and
rejects everything else. No behaviour change on a legitimate cache.

Adds tests/test_ffi_cache_filter.py: an arbitrary-callable gadget and a
cffi.model helper function are both rejected, and a legitimate cache still loads.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant