https://gustedt.wordpress.com/2023/06/10/enforced-bounds-checking-for-frozen-function-interfaces/ Skip to content Jens Gustedt's Blog Enforced bounds checking for frozen function interfaces Many functions in the C library and other legacy codes have interfaces that don't permit the current bounds checking syntax for array parameters, but that are frozen in time such that nobody will ever be able to change and improve them. In this example we go with void* memcpy(void*restrict s1, void const*restrict s2, size_t n); but in fact any existing interface that has size parameters after array or pointer parameters that depend on them has similar problems. The intent of this post is to show how the corresponding header can be easily tuned, such that modern compilers are able to provide static analysis that diagnoses calls that receive buffers that are too small compared to n. The trick to do so is just a two-level wrapper that interchanges call arguments, nothing very deep, and not something that compilers wouldn't be able to perform if we just had means to specify this property in the language. Here, the available interface features allow to encode important information about the function arguments of memcpy: * Both arguments may be pointers or arrays of arbitrary object types (because the types are void) * The buffer pointed to by the first argument might be modified by the function (because there is no qualifier on the base type). * The buffer pointed to by the second argument will not be modified by the function (because there is a const qualifier on the base type). * The pointed-to buffers must not overlap (because both are qualified with restrict) Not visible in that syntax is the imperative that both buffers have to be at least n bytes wide. If we just reorder the parameters and transform them into arrays we can give an alternative interface that expresses this requirement as well: [[__maybe_unused__]] static inline void* memcpy_swpd(size_t n, unsigned char s1[restrict static n], unsigned char const s2[restrict static n]) [[__unsequenced__]] { // This captures a possible pre-existing macro for memcpy return memcpy(s1, s2, n); } Unfortunately, we have to use the type unsigned char, which is C's native type for bytes, instead of void, so the function cannot be called as easily as memcpy. Also this interface cannot directly be used instead of the C library call, and so we cannot use it to identify bounds errors in existing code. With a macro definition for memcpy we can change that #define memcpy(S1, S2, N) \ memcpy_swpd((N), \ /* Make sure no qualification gets lost */ \ (void*){ (S1), }, \ /* Make sure no volatile gets lost */ \ (void const*){ (S2), }) So this takes the arguments in the order in which memcpy expects them and dispatches them to a call to memcyp_swpd There is a complication, because the original functions has void pointers and we want the macro to accept the same arguments as the original memcpy. So here we have to convert the arguments to void pointers such that this function can be called without troubles. We don't want to use simple casts to void*, for example, because that would cast away all other type checks that we still want to maintain. For example with casts we would not be able to detect if the arguments were non-zero integers, errors that are easily detected by the function interface. Therefore we use compound literals where the arguments S1 and S2 are initializers. By that, only an argument type that has an implicit conversion to the corresponding void pointer type can be used, all others will be diagnosed. Now whether or not such an approach detects bound errors depends a lot on the compiler that we will actually use to compile our code. Let's look at four trivial examples where memcpy is used to copy a string literal into a compound literal. puts(memcpy((char[6]){ 0 }, "hello", 6)); // Erroneous target buffer, should not be qualified. diagnostic? puts(memcpy((char volatile[6]){ 0 }, "he1lo", 6)); // Erroneous use of target buffer, using 6 where there are only 5. diagnostic? puts(memcpy((char[5]){ 0 }, "he2lo", 6)); // Erroneous use of source buffer, using 7 where there are only 6. diagnostic? puts(memcpy((char[7]){ 0 }, "he3lo", 7)); Here, all the arguments to the calls have a fixed array size and type, and so errors can in principle be detected at compile time. If you test this code with your favorite compiler you should at least see a diagnostic for the second call, indicating that the volatile qualification is not appropriate. The other two errors are not so easily detected, although for this special case of a C library function the compiler could have enough knowledge about the expected buffer sizes. In fact, for the third call the compound literal is too small, for the fourth it is the string literal. Indeed, a recent clang 17 compiler detects the error in the third call, but not the fourth: generic.c:41:8: warning: 'memcpy' will always overflow; destination buffer has size 5, but size argument is 6 [-Wfortify-source] 41 | puts(memcpy((char[5]){ 0 }, "he2lo", 6)); | ^ Gcc 13 is a bit better, here, and detects both: generic.c:41:8: warning: 'memcpy' forming offset 5 is out of the bounds [0, 5] of object '({anonymous})' with type 'char[5]' [-Warray-bounds=] 41 | puts(memcpy((char[5]){ 0 }, "he2lo", 6)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ generic.c:41:26: note: '({anonymous})' declared here 41 | puts(memcpy((char[5]){ 0 }, "he2lo", 6)); | ^ generic.c:43:8: warning: 'memcpy' forming offset 6 is out of the bounds [0, 6] [-Warray-bounds=] 43 | puts(memcpy((char[7]){ 0 }, "he3lo", 7)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now if we compile the same test with the macro replacement as described above, we expose the errors in the third and fourth usage. Gcc 13 is indeed able to issue diagnostics, here for the third call: generic.h:394:1: warning: 'memcpy_swpd' accessing 6 bytes in a region of size 5 [-Wstringop-overflow=] 394 | memcpy_swpd((N), \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 395 | /* Make sure no qualification gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 396 | (void*){ (S1), }, \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 397 | /* Make sure no volatile gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 398 | (void const*){ (S2), }) | ~~~~~~~~~~~~~~~~~~~~~~~ generic.c:48:8: note: in expansion of macro 'memcpy' 48 | puts(memcpy((char[5]){ 0 }, "he2lo", 6)); | ^~~~~~ generic.h:394:1: note: referencing argument 2 of type 'unsigned char[]' 394 | memcpy_swpd((N), \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 395 | /* Make sure no qualification gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 396 | (void*){ (S1), }, \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 397 | /* Make sure no volatile gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 398 | (void const*){ (S2), }) | ~~~~~~~~~~~~~~~~~~~~~~~ generic.c:48:8: note: in expansion of macro 'memcpy' 48 | puts(memcpy((char[5]){ 0 }, "he2lo", 6)); | ^~~~~~ generic.h:394:1: note: referencing argument 3 of type 'const unsigned char[]' 394 | memcpy_swpd((N), \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 395 | /* Make sure no qualification gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 396 | (void*){ (S1), }, \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 397 | /* Make sure no volatile gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 398 | (void const*){ (S2), }) | ~~~~~~~~~~~~~~~~~~~~~~~ generic.c:48:8: note: in expansion of macro 'memcpy' 48 | puts(memcpy((char[5]){ 0 }, "he2lo", 6)); | ^~~~~~ generic.h:368:7: note: in a call to function 'memcpy_swpd' 368 | void* memcpy_swpd(size_t n, | ^~~~~~~~~~~ This might be a bit verbose, but it clearly identifies the problem. For the fourth call we get: generic.h:394:1: warning: 'memcpy_swpd' reading 7 bytes from a region of size 6 [-Wstringop-overread] 394 | memcpy_swpd((N), \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 395 | /* Make sure no qualification gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 396 | (void*){ (S1), }, \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 397 | /* Make sure no volatile gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 398 | (void const*){ (S2), }) | ~~~~~~~~~~~~~~~~~~~~~~~ generic.c:50:8: note: in expansion of macro 'memcpy' 50 | puts(memcpy((char[7]){ 0 }, "he3lo", 7)); | ^~~~~~ generic.h:394:1: note: referencing argument 3 of type 'const unsigned char[]' 394 | memcpy_swpd((N), \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 395 | /* Make sure no qualification gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 396 | (void*){ (S1), }, \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 397 | /* Make sure no volatile gets lost */ \ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 398 | (void const*){ (S2), }) | ~~~~~~~~~~~~~~~~~~~~~~~ generic.c:50:8: note: in expansion of macro 'memcpy' 50 | puts(memcpy((char[7]){ 0 }, "he3lo", 7)); | ^~~~~~ generic.h:368:7: note: in a call to function 'memcpy_swpd' 368 | void* memcpy_swpd(size_t n, | ^~~~~~~~~~~ So the compiler is in fact capable of telling us that here a read access (other then for the previous that was a potential write access) moves beyond the object. Unfortunately clang 17 is not able to detect these errors and gives no diagnostic. You may also have been wondering what the weird additions enclosed in [[ ]] to the definition of memcpy_swpd are about. These are so-called attributes, a new feature in C23. The first maybe_unused applies to the function definition as a whole to inform the compiler that indeed this function might never be used in the current translation unit. This avoids diagnostics for static identifiers in header files. The second, unsequenced is more involved. It attributes a property to the function type, namely that this function only accesses * its arguments, and * any objects that are reachable through pointer arguments. No other state of the program, such as global variables, is affected. This is an extension of what in CS usually would be called a pure function. As a consequence, calls to this function may be moved around, e.g hoisted out of a loop, as long as the data dependencies for the call arguments and the return value are not changed. Both attributes are specified with leading pairs of underscores, to avoid that they macro-expand in case that the user had a macro with the same name. Advertisement Share this: * Share * * Email * Print * Facebook * Twitter * Related [1852c]Author Jens GustedtPosted on June 10, 2023June 10, 2023 Categories C23, compiler optimization 2 thoughts on "Enforced bounds checking for frozen function interfaces" 1. [pict] Marquis Biggs says: June 11, 2023 at 17:47 Is it possible to use the [static n] syntax on opaque types? Clang gives me an error about an incomplete array type. Reply 1. [1852] Jens Gustedt says: June 12, 2023 at 09:14 No, unfortunately not, for the array notion the type has to be complete for the syntax to be valid. Basically to form an array, the compiler has to know the size of the base type. Therefore the deviation through arrays of base type unsigned char in the memcpy example. Reply Leave a Reply Cancel reply Enter your comment here... [ ] Please log in using one of these methods to post your comment: * * Gravatar Email (required) (Address never made public) [ ] Name (required) [ ] Website [ ] WordPress.com Logo You are commenting using your WordPress.com account. ( Log Out / Change ) Facebook photo You are commenting using your Facebook account. ( Log Out / Change ) Cancel Connecting to %s [ ] Notify me of new comments via email. [ ] Notify me of new posts via email. [Post Comment] [ ] [ ] [ ] [ ] [ ] [ ] [ ] D[ ] This site uses Akismet to reduce spam. Learn how your comment data is processed. Post navigation Previous Previous post: Dealing with overflow [1852c43e] Mastodon C as a language [gustedt-modernc-hi-band] Copyright (c) 2010-2022 Jens Gustedt, Strasbourg, France P99 macros for C99 emulation of C11 Categories * C++ (10) * C11 (43) + defects (11) + feature request (2) + library (9) * C17 (10) * C2x (14) + C23 (7) * C99 (69) + integers (11) + language (31) + P99 (17) + preprocessor (12) + syntax (15) * compiler optimization (5) * core (3) * lock structures (8) * Modular C (5) * POSIX (13) + linux (2) * rants (5) * Uncategorized (1) Recent Posts * Enforced bounds checking for frozen function interfaces * Dealing with overflow * Checked integer arithmetic in the prospect of C23 * C23 implications for C libraries * A defer feature using lambda expressions Top Posts & Pages * Enforced bounds checking for frozen function interfaces * Modern C, Second Edition * Default arguments for C99 * A defer feature using lambda expressions * inline functions as good as templates * Why sem_t is not suited as an atomic counter * Detect empty macro arguments * VLA as function arguments * A Common C/C++ Core Specification rev 2 * Emulating C11 compiler features with gcc: _Generic Search for: [ ] Search related * C99 * c threads * preprocessor Follow Blog via Email Enter your email address to follow this blog and receive notifications of new posts by email. Email Address: [ ] Follow Join 192 other subscribers Meta * Register * Log in * Entries feed * Comments feed * WordPress.com Archives * June 2023 * December 2022 * January 2022 * October 2021 * January 2021 * December 2020 * November 2020 * June 2020 * May 2020 * March 2020 * September 2019 * August 2018 * June 2018 * April 2018 * August 2017 * May 2017 * March 2017 * January 2017 * November 2016 * September 2016 * August 2016 * July 2016 * July 2015 * May 2015 * April 2015 * February 2015 * October 2014 * September 2014 * April 2014 * December 2013 * October 2013 * August 2013 * July 2013 * February 2013 * December 2012 * November 2012 * October 2012 * August 2012 * July 2012 * May 2012 * April 2012 * March 2012 * February 2012 * January 2012 * December 2011 * November 2011 * October 2011 * July 2011 * June 2011 * March 2011 * February 2011 * January 2011 * December 2010 * November 2010 * October 2010 * September 2010 * August 2010 * July 2010 * June 2010 RSS RSS Feed RSS - Posts RSS Feed RSS - Comments Jens Gustedt's Blog Create a free website or blog at WordPress.com. [Close and accept] Privacy & Cookies: This site uses cookies. By continuing to use this website, you agree to their use. To find out more, including how to control cookies, see here: Cookie Policy * Follow Following + [wpcom-] Jens Gustedt's Blog Join 192 other followers [ ] Sign me up + Already have a WordPress.com account? Log in now. * + [wpcom-] Jens Gustedt's Blog + Customize + Follow Following + Sign up + Log in + Copy shortlink + Report this content + View post in Reader + Manage subscriptions + Collapse this bar [b]