Skip to content

Fix segfault when a formatted insertion produces zero characters - #37

Open
afonsojanu wants to merge 1 commit into
JacksonAllan:mainfrom
afonsojanu:fix/str-push-fmt-empty-arg-segfault
Open

afonsojanu wants to merge 1 commit into
JacksonAllan:mainfrom
afonsojanu:fix/str-push-fmt-empty-arg-segfault

Conversation

@afonsojanu

Copy link
Copy Markdown

Fixes #36.

cc_str_insert_wrapped_fmt_args (the function behind push_fmt and insert_fmt) first counts up the total length of everything it needs to format and insert, then, if that total exceeds the current capacity, grows the buffer, and only then does the actual memmove/insert.

The problem is when the total comes out to zero, for instance push_fmt(&s, "") on a container that hasn't allocated anything yet. cc_str_size(cntr) + total > cc_str_cap(cntr) is 0 + 0 > 0, which is false, so the growth step is skipped entirely. Execution falls straight through to the memmove and to cc_str_hdr(cntr)->size += total. For a never-allocated string, cntr still points at CC's shared placeholder for an empty string, which lives in read-only memory, so both of those touch memory that can't be written to and the process crashes.

cc_str_insert_n already has a guard for exactly this situation: it returns immediately when n == 0, before it ever looks at the container's capacity. I added the equivalent check to cc_str_insert_wrapped_fmt_args, right after the length-counting loop and before the growth step, so a zero-length formatted insertion is a no-op rather than falling through to code that assumes there's something to write.

I reproduced the crash from the issue under ASan first (SIGBUS in cc_str_insert_wrapped_fmt_args, matching what DevSolar described), confirmed the fix removes it, and added a test to unit_tests.c covering push_fmt with an empty string across all three of CC's string element types (char, char16_t, char32_t). The full unit test suite still passes clean under ASan/UBSan (including the fault-injection pass that simulates realloc failures), and tests_against_stl.cpp still compiles against the patched header.

cc_str_insert_wrapped_fmt_args (which backs push_fmt and insert_fmt)
counts the total length of all formatted arguments, then makes room
for them and shifts the string's tail (including the null terminator)
to open up space. When that total comes out to zero, e.g. pushing an
empty string into a string container that has never had any storage
allocated, the "make room" check passes trivially (0 + 0 is not
greater than 0), so no allocation happens, and the code falls through
straight to the memmove and header update.

For a freshly initialized string, the container still points at a
shared, read-only placeholder used to represent an empty, unallocated
string. Writing to it (even a self-copy of the terminator, or just
incrementing size by zero) crashes.

cc_str_insert_n already guards against this by returning early when
the number of elements to insert is zero. This adds the same early
return to cc_str_insert_wrapped_fmt_args, and a regression test
covering push_fmt with an empty string for all three of CC's string
element types.
@DevSolar

Copy link
Copy Markdown

Thanks for looking into the mechanics on how to fix this. +1 for copying a mechanic used elsewhere already.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pushing empty string segfaults

2 participants