python / python/cpython

Use-after-free in _Unpickler_ReadIntoFromFile: temporary memoryview passed to readinto() can outlive its buffer

オープン
#151,046 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

extension-modules type-crash
主要言語
Python
スター
77.2k
フォーク
35.9k
PR マージ指標
PR 指標を取得中

説明

Bug report

Bug description

The C implementation of pickle.Unpickler, when reading from a file-like
object that provides readinto(), hands that method a temporary memoryview
created over an internal buffer:

PyObject *buf_obj = PyMemoryView_FromMemory(buf, n, PyBUF_WRITE);
...
PyObject *read_size_obj = _Pickle_FastCall(self->readinto, buf_obj);

(Modules/_pickle.c, _Unpickler_ReadIntoFromFile.)

buf points into a short-lived buffer (e.g. the bytes object allocated in
load_counted_binbytes, which may also be reallocated by _PyBytes_Resize or
freed when unpickling ends). The memoryview is never released or invalidated
after readinto() returns. A readinto() implementation that keeps a
reference to the view can therefore use it to read or write the buffer after
it has been freed, which is a use-after-free at the C level.

This only requires a pure-Python file-like object -- no ctypes. A pure-Python
program should not be able to make the interpreter read or write freed memory.

Reproducer

import pickle, struct, gc

stashed = []

class EvilFile:
    def __init__(self):
        self._h = b"\x80\x05" + b"\x8e" + struct.pack("<Q", 200_000)
        self._p = 0
    def read(self, n=-1):
        d = self._h[self._p:] if (n is None or n < 0) else self._h[self._p:self._p+n]
        self._p += len(d); return d
    def readline(self):
        return self.read(-1)
    def readinto(self, view):
        stashed.append(view)             # keep the view past readinto()
        view[:] = b"A" * len(view); return len(view)

up = pickle.Unpickler(EvilFile())
try:
    up.load()                            # stream ends after the payload
except EOFError:
    pass
del up; gc.collect()                     # free the backing buffer
_ = [bytes(200_000) for _ in range(8)]   # churn the allocator
stashed[0][0]                            # <-- use-after-free read

On a --with-address-sanitizer --with-pydebug build this reports a clean
heap-use-after-free (READ in unpack_single, the buffer freed via
Pdata_dealloc and originally allocated in load_counted_binbytes).

Root cause and fix direction

The view is a non-owning window over a raw pointer that the unpickler does not
keep alive. Other CPython sites that drive a user readinto() (e.g.
_io.RawIOBase.read()) hand out an owning object (a bytearray) whose buffer
protocol prevents it from being freed while still exported. The pickle path
should release the temporary memoryview as soon as readinto() returns, so a
surviving reference raises ValueError: operation forbidden on released memoryview object instead of dereferencing freed memory.

I have a patch with a regression test and will open a PR.

(For context: this was originally raised privately with the Python Security
Response Team, who advised opening a public issue.)

CPython versions tested on

3.16.0a0 (main, commit 5755d0f).

Operating systems tested on

macOS (arm64), --with-address-sanitizer --with-pydebug build.

Linked PRs
  • gh-151048

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

Modules/_pickle.c の _Unpickler_ReadIntoFromFile から始め、readinto() の戻り後に一時的な memoryview がどのように扱われるかを確認します。提供されている EvilFile の再現コードを ASAN、pydebug ビルドで使用し、その後、言及されている回帰テストを追加または確認します。保持されたビューが解放済みバッファーにアクセスできず、期待される解放済み memoryview エラーを発生させれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
c, python
領域
backend, security
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
明確に書かれている
初心者へのやさしさ
20/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。