pico_printf exhibits undefined behavior with null %s argument, even with 0 precision #3167

Open
opened 2026-09-02 23:00:43 -05:00 by BurntPizza · 0 comments
BurntPizza commented 2026-09-02 23:00:43 -05:00 (Migrated from github.com)

I wanted to get a team member's thoughts on this before just submitting a PR.

Summary: printf("%.*s", 0, (char*)0) dereferences the null pointer despite the 0 precision sub-specifier. I propose a small change to _vsnprintf to instead define the behavior to print nothing, consistent with other implementations.
EDIT: technically, functions like sprintf would still print the nul terminator (compared to, say, snprintf with size 0), I forgot because I've actually been using vfctprintf, which doesn't output nuls.

I ran into this through a combination of trying to use 'pointer-and-length' strings instead of nul-terminated ones in my recent project, and compiling with UBSan. Without UBSan, the behavior is (so far) actually as desired, so I would understand if this is considered very low priority. But it would be nice to do less patching of my (and my teammate's) copies of the SDK in the future.
Specifically, I've been using typedef struct { char* data; size_t len; } str; in my code and it works nicely with things like parsing. One nicety is that zero-initializing it is a valid empty string, for the purposes of my other string processing functions. But after I started using UBSan, I found out that printf("%.*s", s.len, s.data) derefs the pointer, even with the zero length. To be fair, this seems to be allowed by the standard, but every libc I've looked at (including newlib and picolibc, as well as glibc and musl) define the behavior. So it would be nice to be consistent.

I saw https://github.com/raspberrypi/pico-sdk/issues/2694, so one option would be to punt and wait for that update. Unfortunately for me, I would/will have to try to submit a PR to eyalroz as their fork currently would print "(null)" unconditionally, ignoring the precision.

The simplest fix (which I use locally) is to add if (!p) p = ""; in between the lines: github.com/raspberrypi/pico-sdk@98a542c1a6/src/rp2_common/pico_printf/printf.c (L810-L811)

Of course it could instead be p = "(null)" for debugability in cases someone passes a null pointer accidentally to %s (and aren't using UBSan), plus full consistency with other implementations. This would still print the desired 0 characters with a precision of 0. It just takes 6 more bytes.

Related UB note: I can submit a separate issue/PR for this if appropriate, but I thought I'd quickly seek comments about it here: another patch I had to make is adding __attribute__((no_sanitize("undefined"))) to runtime_init as the (*p)(); on line 206 also trips UBSan. I didn't know what to do about it that deep in pre-main code so I ignored it with the attribute. This and the _vsnprintf are the only instances I've found in the sdk proper so far. (however tinyusb 0.18 derefs null all the time!) Granted there are many parts of the sdk I've yet to use, but I figure if I was a maintainer, I'd want to know about instances of UB.

Thanks in advance, please let me know what you think.

I wanted to get a team member's thoughts on this before just submitting a PR. Summary: `printf("%.*s", 0, (char*)0)` dereferences the null pointer despite the `0` precision sub-specifier. I propose a small change to `_vsnprintf` to instead define the behavior to print nothing, consistent with other implementations. EDIT: technically, functions like `sprintf` would still print the nul terminator (compared to, say, `snprintf` with size 0), I forgot because I've actually been using `vfctprintf`, which doesn't output nuls. I ran into this through a combination of trying to use 'pointer-and-length' strings instead of nul-terminated ones in my recent project, and compiling with UBSan. Without UBSan, the behavior is (so far) actually as desired, so I would understand if this is considered very low priority. But it would be nice to do less patching of my (and my teammate's) copies of the SDK in the future. Specifically, I've been using `typedef struct { char* data; size_t len; } str;` in my code and it works nicely with things like parsing. One nicety is that zero-initializing it is a valid empty string, for the purposes of my other string processing functions. But after I started using UBSan, I found out that `printf("%.*s", s.len, s.data)` derefs the pointer, even with the zero length. To be fair, this seems to be allowed by the standard, but every libc I've looked at (including [newlib](https://github.com/anza-xyz/newlib/blob/f0f8003daa3f71a1a3e2f067f8d5bb46e2756514/newlib/libc/stdio/vfprintf.c#L1440) and [picolibc](https://github.com/picolibc/picolibc/blob/036919b76d7124f25f1d82554a06c921708cd4b1/libc/stdio/vfprintf_str.c#L81), as well as [glibc](https://codebrowser.dev/glibc/glibc/stdio-common/vfprintf-process-arg.c.html#410) and [musl](https://git.musl-libc.org/cgit/musl/tree/src/stdio/vfprintf.c#n599)) define the behavior. So it would be nice to be consistent. I saw https://github.com/raspberrypi/pico-sdk/issues/2694, so one option would be to punt and wait for that update. Unfortunately for me, I would/will have to try to submit a PR to eyalroz as their fork currently would print `"(null)"` [unconditionally](https://github.com/eyalroz/printf/blob/22318ed6bd87b0d597b4a4868d5b00daf66540fd/src/printf/printf.c#L1506), ignoring the precision. The simplest fix (which I use locally) is to add `if (!p) p = "";` in between the lines: https://github.com/raspberrypi/pico-sdk/blob/98a542c1a62fb549ffb5d66a3e5892b06276b670/src/rp2_common/pico_printf/printf.c#L810-L811 Of course it could instead be `p = "(null)"` for debugability in cases someone passes a null pointer accidentally to `%s` (and aren't using UBSan), plus full consistency with other implementations. This would still print the desired 0 characters with a precision of 0. It just takes 6 more bytes. Related UB note: I can submit a separate issue/PR for this if appropriate, but I thought I'd quickly seek comments about it here: another patch I had to make is adding `__attribute__((no_sanitize("undefined")))` to [runtime_init](https://github.com/raspberrypi/pico-sdk/blob/98a542c1a62fb549ffb5d66a3e5892b06276b670/src/rp2_common/pico_clib_interface/newlib_interface.c#L181) as the `(*p)();` on line 206 also trips UBSan. I didn't know what to do about it that deep in pre-main code so I ignored it with the attribute. This and the `_vsnprintf` are the only instances I've found in the sdk proper so far. (however tinyusb 0.18 derefs null all the time!) Granted there are many parts of the sdk I've yet to use, but I figure if I was a maintainer, I'd want to know about instances of UB. Thanks in advance, please let me know what you think.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-raspberrypi/pico-sdk#3167
No description provided.