Skip to content

[Fix] Defensive infinite loop guard and UTF-8 check in C API - #20348

Merged
Mytherin merged 5 commits into
duckdb:v1.4-andiumfrom
taniabogatsch:defensive-checks
Jan 7, 2026
Merged

Mytherin merged 5 commits into
duckdb:v1.4-andiumfrom
taniabogatsch:defensive-checks

Conversation

@taniabogatsch

Copy link
Copy Markdown
Member

Related issue: https://github.com/duckdblabs/duckdb-internal/issues/7002, where invalid UTF-8 strings can cause hangs during scalar function execution.

The hang is due to incorrect API usage, but we did not sufficiently enforce it. Thus, it was possible for invalid UTF-8 to "slip through" and later on cause a hang. All vectors with a logical type VARCHAR are expected to contain valid UTF-8, which is now enforced in the C API.

@lnkuiper - do you think the performance implication of calling auto utf_type = duckdb::Utf8Proc::Analyze(str, str_len, &reason, &pos); for each string here is acceptable, or should we expose an additional function? Or maybe only perform the check behind a #ifdef DEBUG guard?

@taniabogatsch

Copy link
Copy Markdown
Member Author

After thinking some more about this I have doubts about adding the check there without a #ifdef DEBUG guard - maybe we need to be more aggressive with the documentation, but I don't like the performance implication of always performing UTF-8 validity analysis...

@lnkuiper

lnkuiper commented Dec 31, 2025

Copy link
Copy Markdown
Member

do you think the performance implication of calling auto utf_type = duckdb::Utf8Proc::Analyze(str, str_len, &reason, &pos); for each string here is acceptable, or should we expose an additional function? Or maybe only perform the check behind a #ifdef DEBUG guard?

Acceptable and necessary. DuckDB expects any VARCHAR that has entered the system to be valid UTF-8. Allowing users to enter unexpected data into the system can cause unexpected behaviour (e.g., the infinite loop, which could be abused to cause the system to hang: potential denial of service).

If we don't add the check here ourselves, any application developer that uses DuckDB and handles user data will have to add it themselves, as they wouldn't ever want DuckDB to exhibit unexpected behaviour.

Just as an example: strings in Parquet are required to be valid UTF-8 as per the Parquet spec. Despite this, we still run UTF-8 validation upon reading strings from Parquet, because user input cannot be trusted. It's not free, but it's also not prohibitively expensive - out Parquet reader performance is fine.

EDIT: you changes look good to me, but let's discuss

@lorenzowritescode lorenzowritescode left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any benchmarks that would alert you if this change has significant performance impact?
In general, I would err on the side of safety here. Bugs caused by issues like this are very hard to track down.

Imagine we only added the assertion in the GetResultLength function but still allowed invalid UTF-8 VARCHARs, a similar issue could pop up later in a different string function.

Looking at the source here https://github.com/duckdb/duckdb/blob/main/third_party/utf8proc/utf8proc_wrapper.cpp#L80-L87 it looks like this function has an optimisation to quickly skip over ASCII chars so for ASCII-only strings the impact should be minimal (correct me if I am wrong here - not exactly the sharpest C/C++ programmer 😆 )

@lnkuiper

Copy link
Copy Markdown
Member

Looking at the source here https://github.com/duckdb/duckdb/blob/main/third_party/utf8proc/utf8proc_wrapper.cpp#L80-L87 it looks like this function has an optimisation to quickly skip over ASCII chars so for ASCII-only strings the impact should be minimal (correct me if I am wrong here - not exactly the sharpest C/C++ programmer 😆 )

I added this a while ago. The optimisation specifically improves performance for longer strings by checking 8 bytes at a time. The performance of verifying UTF-8 strings should be relatively OK, but above all it's about correctness and safety.

@taniabogatsch
taniabogatsch marked this pull request as draft January 4, 2026 10:49
@EtgarDev

EtgarDev commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

nice! I also prefer throwing an exception there :)
BTW I noticed that the same problem probably exists also in translate.cpp and utf8proc_wrapper.cpp

@taniabogatsch

taniabogatsch commented Jan 5, 2026

Copy link
Copy Markdown
Member Author

If we don't add the check here ourselves, any application developer that uses DuckDB and handles user data will have to add it themselves, as they wouldn't ever want DuckDB to exhibit unexpected behaviour.

@lnkuiper - I think in this case application developers still have to add an extra check because, as-of now, duckdb_vector_assign_string_element cannot return an error. So while we won't assign invalid UTF-8, we also don't tell the user that something went wrong and eventually pass invalid data chunks into DuckDB.

I propose that in this PR I'll elaborate more in the function comment that when using duckdb_vector_assign_string_element[_len] users must provide valid UTF-8 for VARCHAR. I'll also open a follow-up PR to main exposing duckdb_safe_vector_assign_string_element[_len], which returns error data, if VARCHAR and the string is not valid UTF-8, and mark the unsafe functions as outdated.

Imagine we only added the assertion in the GetResultLength function but still allowed invalid UTF-8 VARCHARs, a similar issue could pop up later in a different string function.

@lorenzowritescode - agreed, we need to both prevent the loop and ensure it is much harder to enter invalid input.

BTW I noticed that the same problem probably exists also in translate.cpp and utf8proc_wrapper.cpp

@EtgarDev - thanks! I'll have a look :)

@lnkuiper

lnkuiper commented Jan 5, 2026

Copy link
Copy Markdown
Member

@taniabogatsch Maybe we can return the error in duckdb_appender_flush then? This one can return an error, and then we could verify the entire Vector in one go

@taniabogatsch

Copy link
Copy Markdown
Member Author

Hmm, we could do that - however, this function can be used in many places, for example also when executing custom UDFs. The original issue was found during scalar UDF execution.

We've had some issues w.r.t. when and where to properly return errors in the C API. @Maxxen has been working on more context-awareness and ways to pass and inspect contexts, which can then contain errors (next to other things). However, we don't have that (yet).

We could also refactor the duckdb_vector into a wrapper, as we do with other C API structures, like the appender. That would have no external effects and we could then set the error in the vector itself. Then, we could expose duckdb_vector_get_error to periodically check the error...

@lnkuiper

lnkuiper commented Jan 5, 2026

Copy link
Copy Markdown
Member

I see. I understand that it's difficult right now to get this to throw, so I am fine with improving our docs, but this really should always throw in the future.

We could consider performing this check after custom UDF execution too. We don't want user strings to be a potential gateway to introducing unexpected behaviour.

@taniabogatsch

Copy link
Copy Markdown
Member Author

Thanks a lot for the input + discussion! Coming back to this after thinking about it a bit further. The C API is kind of different from, e.g., parquet files. It is not directly exposed to end users, but other developers. For example, we hand out raw void * pointers for writing to vectors, and have a lot of "unsafe" functions / behaviors.

That being said, preventing each client + extension from having to implement these valid UTF-8 guards themselves is very convenient and a sensible choice. And I fully agree with us throwing on incorrect input, plus that we should try our best to prevent (accidental) incorrect input through the C API.

Since we cannot throw in C API functions and duckdb_vector_assign_string_element_[len] cannot return any error information, I've removed the check from it now and further elaborated on the documentation. I felt like leaving it as-it makes other errors even harder to detect ("I am sure I called the function to set the string but it did nothing!").

Once we merge this PR + andium into main, I'll add new duckdb_vector_safe_assign_... functions and outdate the current ones. Since v1.4 is a bug fix branch I unfortunately cannot change the C API here. So until we've patched this on main the additional guard in the GetResultLength function and the improved documentation must suffice, I'd say.

@taniabogatsch
taniabogatsch marked this pull request as ready for review January 5, 2026 20:41
Comment thread src/function/scalar/string/caseconvert.cpp
@taniabogatsch
taniabogatsch marked this pull request as draft January 6, 2026 09:17
@taniabogatsch
taniabogatsch marked this pull request as ready for review January 6, 2026 11:23
@abramk

abramk commented Jan 6, 2026

Copy link
Copy Markdown
Contributor

This PR is of interest to us! We have seen instances of queries never finishes (symptoms are one or two CPU cores consumed at 100% and remaining that way forever or the query just never returns). We know some of our usage has invalid unicode strings. We evaluated doing a pretest for invalid unicode in Golang but deemed it very expensive. We would be very interested if you all exposed a high-speed unicode validator or a separate API in the C-appender that does unicode validation of its inputs (also for the variant that accepts a full datachunk).

@taniabogatsch

Copy link
Copy Markdown
Member Author

you all exposed a high-speed unicode validator or a separate API in the C-appender that does unicode validation of its inputs (also for the variant that accepts a full datachunk).

The current plan is to expose a new C API function for writing UTF-8-validated strings to data chunks, which should then cover all cases where we create (and later ingest) such data chunks.

@Mytherin
Mytherin merged commit 959c9c7 into duckdb:v1.4-andium Jan 7, 2026
62 checks passed
@Mytherin

Mytherin commented Jan 7, 2026

Copy link
Copy Markdown
Collaborator

Thanks!

@taniabogatsch
taniabogatsch deleted the defensive-checks branch January 7, 2026 12:01
github-actions Bot pushed a commit to duckdb/duckdb-r that referenced this pull request Jan 7, 2026
[Fix] Defensive infinite loop guard and UTF-8 check in C API (duckdb/duckdb#20348)
Fixup BRANCHES_TO_BE_CACHED, vars are not available on PRs, so env it is (duckdb/duckdb#20411)
Don't add a semicolon to final query when splitting statements (duckdb/duckdb#20401)
github-actions Bot added a commit to duckdb/duckdb-r that referenced this pull request Jan 7, 2026
[Fix] Defensive infinite loop guard and UTF-8 check in C API (duckdb/duckdb#20348)
Fixup BRANCHES_TO_BE_CACHED, vars are not available on PRs, so env it is (duckdb/duckdb#20411)
Don't add a semicolon to final query when splitting statements (duckdb/duckdb#20401)

Co-authored-by: krlmlr <krlmlr@users.noreply.github.com>
@taniabogatsch

Copy link
Copy Markdown
Member Author

Here's the follow-up PR with the new safe C API function: #20467

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants