cpython icon indicating copy to clipboard operation
cpython copied to clipboard

Type confusion in `zoneinfo_ZoneInfo_impl` via overridden cache `setdefault()`

Open jackfromeast opened this issue 1 month ago • 1 comments

What happened?

Handler zoneinfo_ZoneInfo_impl() calls weak_cache.setdefault(key, tmp) and blindly treats the returned object as a ZoneInfo, immediately casting to PyZoneInfo_ZoneInfo and writing to its fields. A subclass can swap _weak_cache with a dict-like object whose setdefault() returns a non-ZoneInfo (e.g., an int), causing type confusion and subsequent memory corruption/crash under ASan. A minimal PoC overrides setdefault() to return the wrong type and triggers a buffer overflow during teardown.

Proof of Concept:

import zoneinfo

class EvilCache(dict):
    def setdefault(self, key, value):
        return 1337  # wrong type

class Evil(zoneinfo.ZoneInfo):
    pass

Evil._weak_cache = EvilCache()  # override the inherited cache

Evil("UTC")

Affected Versions:

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30) ASAN 1
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0] ASAN 1
Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0] ASAN 1
Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0] ASAN 1
Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0] ASAN 1
Python 3.14.0+ (heads/3.14:889e918, Oct 17 2025, 11:23:02) [GCC 13.3.0] ASAN 1
Python 3.15.0a1+ (heads/main:fbf0843, Oct 17 2025, 11:23:37) [GCC 13.3.0] ASAN 1

Related Code Snippet

static PyObject *
zoneinfo_ZoneInfo_impl(PyTypeObject *type, PyObject *key)
/*[clinic end generated code: output=95e61dab86bb95c3 input=ef73d7a83bf8790e]*/
{
	   if (instance == Py_None) {
        Py_DECREF(instance);
        PyObject *tmp = zoneinfo_new_instance(state, type, key);
        if (tmp == NULL) {
            return NULL;
        }

        instance =
            PyObject_CallMethod(weak_cache, "setdefault", "OO", key, tmp);
        Py_DECREF(tmp);
        if (instance == NULL) {
            return NULL;
        }
        
	// Bug: Type Confusion
        ((PyZoneInfo_ZoneInfo *)instance)->source = SOURCE_CACHE;
    }

    update_strong_cache(state, type, key, instance);
    return instance;
}

Sanitizer Report

=================================================================
==1440071==ERROR: AddressSanitizer: global-buffer-overflow on address 0x568a2b530208 at pc 0x568a2ac75512 bp 0x7ffe6e89f060 sp 0x7ffe6e89f050
READ of size 8 at 0x568a2b530208 thread T0
    #0 0x568a2ac75511 in _Py_Dealloc Objects/object.c:3174
    #1 0x568a2acbf5cb in Py_DECREF Include/refcount.h:401
    #2 0x568a2acbf5ea in Py_XDECREF Include/refcount.h:511
    #3 0x568a2acc1a9c in tuple_dealloc Objects/tupleobject.c:213
    #4 0x568a2ac75481 in _Py_Dealloc Objects/object.c:3200
    #5 0x568a2abc2e2b in Py_DECREF Include/refcount.h:401
    #6 0x568a2abc33a8 in Py_XDECREF Include/refcount.h:511
    #7 0x568a2abc533a in code_dealloc Objects/codeobject.c:2423
    #8 0x568a2ac75481 in _Py_Dealloc Objects/object.c:3200
    #9 0x568a2ac0cb65 in Py_DECREF Include/refcount.h:401
    #10 0x568a2ac0f5c9 in func_dealloc Objects/funcobject.c:1158
    #11 0x568a2ac75481 in _Py_Dealloc Objects/object.c:3200
    #12 0x568a2ac40874 in Py_DECREF Include/refcount.h:401
    #13 0x568a2ac40893 in Py_XDECREF Include/refcount.h:511
    #14 0x568a2ac40edf in dictkeys_decref Objects/dictobject.c:462
    #15 0x568a2ac514e9 in clear_lock_held Objects/dictobject.c:2947
    #16 0x568a2ac5168f in PyDict_Clear Objects/dictobject.c:2976
    #17 0x568a2acdca6c in type_clear Objects/typeobject.c:6962
    #18 0x568a2ace181c in subtype_clear Objects/typeobject.c:2690
    #19 0x568a2aee3fe0 in delete_garbage Python/gc.c:1199
    #20 0x568a2aee472c in gc_collect_region Python/gc.c:1814
    #21 0x568a2aee5ce2 in gc_collect_full Python/gc.c:1728
    #22 0x568a2aee7045 in _PyGC_Collect Python/gc.c:2096
    #23 0x568a2aee711a in _PyGC_CollectNoFail Python/gc.c:2137
    #24 0x568a2af5b2dc in finalize_modules Python/pylifecycle.c:1795
    #25 0x568a2af66618 in _Py_Finalize Python/pylifecycle.c:2255
    #26 0x568a2af666f2 in Py_FinalizeEx Python/pylifecycle.c:2378
    #27 0x568a2afc8847 in Py_RunMain Modules/main.c:774
    #28 0x568a2afc8a2e in pymain_main Modules/main.c:802
    #29 0x568a2afc8db3 in Py_BytesMain Modules/main.c:826
    #30 0x568a2aa4c645 in main Programs/python.c:15
    #31 0x7add2d02a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #32 0x7add2d02a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #33 0x568a2aa4c574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

Address 0x568a2b530208 is a wild pointer inside of access range of size 0x000000000008.
SUMMARY: AddressSanitizer: global-buffer-overflow Objects/object.c:3174 in _Py_Dealloc
Shadow bytes around the buggy address:
  0x568a2b52ff80: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530000: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530080: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530100: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530180: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
=>0x568a2b530200: f9[f9]f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530280: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530300: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530380: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530400: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x568a2b530480: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
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
==1440071==ABORTING

jackfromeast avatar Dec 16 '25 02:12 jackfromeast

You probably need a type check like this before the assignment,

		if (!PyObject_TypeCheck(instance, type)) {
		  PyErr_Format(
					   PyExc_TypeError,
					   "%s._weak_cache.setdefault() returned %s, expected %s",
					   ((PyTypeObject *)type)->tp_name,
					   Py_TYPE(instance)->tp_name,
					   type->tp_name
					   );
		  Py_DECREF(instance);
		  return NULL;
		}
		
        ((PyZoneInfo_ZoneInfo *)instance)->source = SOURCE_CACHE;

I have tested this out with Python 3.14 with a local build and works well.

$ ./python ../testing_cpython/zoninfo_sec.py 
Traceback (most recent call last):
  File "/home/anand/projects/cpython/../testing_cpython/zoninfo_sec.py", line 17, in <module>
    Evil("UTC")
    ~~~~^^^^^^^
TypeError: Evil._weak_cache.setdefault() returned int, expected Evil

pythonhacker avatar Dec 16 '25 13:12 pythonhacker