← index #17852Issue #17924
Related · high · value 3.428
QUERY · ISSUE

pin vs stream confusion leads to undefined behavior and Asan diagnostic

openby jepleropened 2025-08-06updated 2025-08-06
bug

Port, board and/or hardware

unix port, coverage variant w/address sanitizer, x86_64 linux

MicroPython version

MicroPython v1.26.0-preview.524.g255d74b5a8 on 2025-08-06; linux [GCC 12.2.0] version

Reproduction

Build with . tools/ci.sh; ci_unix_sanitize_address_build. Then, run the following code (I did it at the repl):

>>> import machine
>>> import ssl
>>> ctx = ssl.SSLContext(1)
>>> p = machine.PinBase()
>>> ctx.wrap_socket(p)

Expected behaviour

ctx.wrap_socket should raise an exception, because a PinBase does not implement the stream protocol. For instance, ctx.wrap_socket(1) throws OSError: stream operation not supported.

Observed behaviour

>>> ctx.wrap_socket(p)
=================================================================
==2165677==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5585c4169a28 at pc 0x5585c3f6a4fb bp 0x7ffe114fe930 sp 0x7ffe114fe928
READ of size 8 at 0x5585c4169a28 thread T0
    #0 0x5585c3f6a4fa in mp_get_stream_raise ../../py/stream.c:102
    #1 0x5585c3fb01cc in ssl_socket_make_new ../../extmod/modtls_mbedtls.c:623
    #2 0x5585c3fb0a30 in ssl_context_wrap_socket ../../extmod/modtls_mbedtls.c:519
    #3 0x5585c3f3c338 in fun_builtin_var_call ../../py/objfun.c:118
    #4 0x5585c3f1c0d8 in mp_call_function_n_kw ../../py/runtime.c:727
    #5 0x5585c3f1ca56 in mp_call_method_n_kw ../../py/runtime.c:743
    #6 0x5585c3f7e435 in mp_execute_bytecode ../../py/vm.c:1069
    #7 0x5585c3f3c700 in fun_bc_call ../../py/objfun.c:295
    #8 0x5585c3f1c0d8 in mp_call_function_n_kw ../../py/runtime.c:727
    #9 0x5585c3f1ca56 in mp_call_method_n_kw ../../py/runtime.c:743
    #10 0x5585c3f7e435 in mp_execute_bytecode ../../py/vm.c:1069
    #11 0x5585c3f3c700 in fun_bc_call ../../py/objfun.c:295
    #12 0x5585c3f1c0d8 in mp_call_function_n_kw ../../py/runtime.c:727
    #13 0x5585c3f2006a in mp_call_function_0 ../../py/runtime.c:701
    #14 0x5585c4091d84 in execute_from_lexer /home/jepler/src/micropython/ports/unix/main.c:162
    #15 0x5585c40923fb in do_repl /home/jepler/src/micropython/ports/unix/main.c:273
    #16 0x5585c4093b3d in main_ /home/jepler/src/micropython/ports/unix/main.c:753
    #17 0x5585c4093d6d in main /home/jepler/src/micropython/ports/unix/main.c:494
    #18 0x7f12f5e46249 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #19 0x7f12f5e46304 in __libc_start_main_impl ../csu/libc-start.c:360
    #20 0x5585c3ec0bf0 in _start (/home/jepler/src/micropython/ports/unix/build-coverage/micropython+0x1a7bf0)

0x5585c4169a28 is located 56 bytes to the left of global variable 'pinbase_singleton' defined in '../../extmod/machine_pinbase.c:43:27' (0x5585c4169a60) of size 8
0x5585c4169a28 is located 0 bytes to the right of global variable 'pinbase_pin_p' defined in '../../extmod/machine_pinbase.c:75:25' (0x5585c4169a20) of size 8
SUMMARY: AddressSanitizer: global-buffer-overflow ../../py/stream.c:102 in mp_get_stream_raise

Pin objects and stream objects both fill the protocol spot. However, their protocol definitions are different and incompatible.

Without ASan, this resulted in an exception (yay!) but this is by luck, because mp_get_stream_raise can't tell that the protocol slot of the object is an incompatible "Pin" protocol (with only ioctl) rather than the standard "Stream" protocol (with read/write/ioctl).

Additional Information

Somewhat related: https://github.com/micropython/micropython/issues/17714 (can pass an object without protocol to machine.time_pulse_us; missing check whether the protocol slot is filled at all).

There's a third leg to the problem, in which this calls read a bunch of times on sys.stdout (due to incorrectly calling through the read function of mp_stream_p_t):

>>> machine.time_pulse_us(sys.stdout, .001)
-2

For once this wasn't from fuzzing. 😜

Code of Conduct

Yes, I agree

CANDIDATE · ISSUE

Clang undefined behavior sanitizer diagnostics (mostly uninteresting??)

openby jepleropened 2025-08-15updated 2025-08-15
bug

Port, board and/or hardware

unix port, coverage build, x86_64 linux, clang-19

MicroPython version

v1.27.0-preview-15-g744270ac1b

Reproduction

perform the undefined behavior sanitizer build but with CC=clang, then try doing pretty much anything (such as starting micropython to the repl)

Expected behaviour

It works and is essentially free of undefined behavior diagnostics.

Observed behaviour

Several classes of diagnostic appear almost immediately.

I investigated two main classes of diagnostic:

  • Applying zero offsets to NULL pointers
  • Calling functions without exactly matching prototypes

Here's an example of each kind:

../../py/map.c:193:37: runtime error: applying zero offset to null pointer
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../py/map.c:193:37 
../../py/stream.c:60:28: runtime error: call to function vfs_posix_file_write through pointer to incorrect function type 'unsigned long (*)(void *, void *, unsigned long, int *)'
/home/jepler/src/micropython/ports/unix/../../extmod/vfs_posix_file.c:129: note: vfs_posix_file_write defined here

These are both classes of "technically forbidden per the C specification but work fine almost always in practice".

The first can be replaced by an extra guard check, but at the possible cost of code. For example,

-    const mp_obj_t *kwargs = args + n_args;
+    const mp_obj_t *kwargs = args ? args + n_args : NULL;

As discussed in the old sanitizer threads, I think this specific behavior is set to become defined ( (NULL+0 is NULL) in a future C standard.

The second is harder to resolve. For instance, this technically means the trick of calling either a read or write func through a function pointer with the read type is incorrect (the prototypes differ only by whether the data argument is const:

    if (flags & MP_STREAM_RW_WRITE) {
        io_func = (io_func_t)stream_p->write;
    } else {
        io_func = stream_p->read;                                  
    }
... mp_uint_t out_sz = io_func(stream, buf, size, errcode); ...

I didn't find a fine grained method to turn off these diagnostics. For instance, the first one is under the general umbrella of "pointer overflow" checks, which includes actual overflow in pointer arithmetic like uint32_t *ptr; ptr[large] when large * sizeof(uint32_t) makes the address wrap around.

Additional Information

I was interested in clang ubsan because the AFLplusplus fuzzer can be run in a mode where it treats sanitizer diagnostics as crashes. However, it defaulted to using clang rather than gcc, so I discovered that it really doesn't like the current state of micropython and so it can't make any interesting findings.

Oh here's a bonus that I found when preparing this issue. It occurs when building an empty list (and, probably, tuple). It results because unsigned subtraction is being used but the intent is to grow the stack by an element. Technically it is an overflowed subtraction so it is undefined behavior. but not interesting. More uninteresting signed overflows appear in vm.c and touching any of them is likely to cause code growth without benefit.

Starting program: /home/jepler/src/micropython/ports/unix/build-coverage/micropython -c '[]'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
../../py/vm.c:832:24: runtime error: subtraction of unsigned offset from 0x7fffffffd920 overflowed to 0x7fffffffd928
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../py/vm.c:832:24 

Code of Conduct

Yes, I agree

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied