RFC: Enforcing Bounds Safety in C (-fbounds-safety)
discourse.llvm.org
discourse.llvm.org
void foo(size_t elem_count, int elems[elem_count]);
Unfortunately this constraints the argument order. C23 fixes that with something like this: void foo(size_t elem_count; int elems[elem_count], elem_count);
(Might be slightly wrong, going from memory.)In both C and C++, it drives me absolutely bonkers that declaration order matters so much.
Point of personal preference, but they're the last languages I use with any regularity where you can't just reference a symbol that will be declared later in the file. In this case, the compiler should be able to figure it out without any of that hinting; the scope of potential valid bindings is in the parentheses. It's a very constrained scope!
Then again, we have so much compute going to LLMs lately, I wonder if we need to revisit our assumptions about how much hardware we have available for compilation. What might a language look like with the assumption we can spread LLM-training levels of hardware among everyone working on a large codebase? Or a compiler that can consult some sort of ML-based cache for reordering decisions? There's a new space of options opening up here.
[1] With main() being the exception.
[2] Latest version being https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3121.pdf.
[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3125.pdf
https://godbolt.org/z/oc6MTWjYd
For your syntax we will probably just also add it is an option.
void f(char (*pa)[7]);
void g(int x, char (*pa)[x]);
void h(int x, char p[static x]);
char a[10];
then f(&a) is a constraint violation, and I believe g(7,&a) might actually be UB, whereas h(7,a) is fine. On the other hand, in the latter case void h(int x, char p[static x]) { p = (char *)&x; }
is legal, so extending that with bounds checks is also not without its problems.It's important to recognize that in plain C the above is simply an aid to someone looking at the header and is no different from
void foo(size_t elem_count, int elems[]);
As such int j;
foo(20, &j);
is fine. Under this RFC this is an error and the call site would trap at runtime (if not picked up a compile time).The RFC also comments on (and handles)
void foo(int elems[] __counted_by(elem_count), size_t elem_count);
Look for references to "delayed parsing". It also handles similar in structs.And compilers do this already to some degree: https://godbolt.org/z/c7xdazKrd
This is a problem because it's trivially easy to have code along the lines of
void foo(int size, int buffer[size]) {
...
int new_base = some_math;
int new_size = some_other_math;
foo(new_size, buffer+new_base);
...
}
That computes the size or base incorrectly, and the compiler will do nothing to stop you walking off the ends.Obviously this RFC just uses the existing syntax if it's there and doesn't need explicit annotations, but the existing syntax requires the size parameter first which many APIs don't have. gcc has an extension to pre-declare a parameter name and type but you can't just use macros to make that work in other or older compilers. They also don't support sizes that aren't just a direct reference to a parameter (e.g. you can't make void some_matrix_func(int size, float matrix[size*size])).
The old GCC extension can also be hidden behind a macro, so this is backwards compatible:
void foo(HIDE(int x;) char buf[HIDE(x)], int x);
This RFC applies to all pointers in all cases, and bounds checking occurs on every pointer access at runtime. So while the annotations clearly overlap, the language semantics are vastly different.
The syntax this RFC uses is based on the standardized syntax for attributes in C.
GCC and Clang have a standard syntax for attributes, which is what this feature uses. Moreover the position semantics of the __attribute__ syntax matches the positional semantics of the standardized C++ attribute syntax, which is (fingers crossed) finally being picked up in C23.
So with that mea culpa let's address the rest.
__counted_by is obviously not standardized - this is an RFC for clang, let alone a spec proposal.
__counted_by is a macro that expands to:
__attribute__((__counted_by__(T)))
Which is the standard syntax for all attributes in clang and gcc.So the question is why the position is different from under SAL. SAL considers the bounds of a pointer to be a feature of the declaration, not the type. This RFC consider pointer bounds to be an attribute of the type itself. Because of that, the syntax of the attribute needs to be in the location for type attributes. You might reasonably respond with "a one off match of this other syntax would have been ok", but the SAL syntax also means that you can only specify one set of bounds per declaration. By having bounds be an attribute of the type you can specify the bounds at each level of indirection in a declaration, so you can have something like:
void dump_string_list(int N, const char * __null_terminated * __counted_by(N) thing);
The other part is that this RFC also allows for opt in wide pointers so you can do void something_else(int N, int * __indexable * __counted_by(N) thing);
Which would presumably not be usable for an existing API, but for internal logic that isn't subject to ABI constraints would allow the adoption of bounds safety without significant source changes. It also highlights the impact on syntactic consistency of having this be an attribute of the type.Now, given the necessary positional difference, reusing the same token as used for MS's SAL would make these incompatible, so code bases would need to use different macro names for each anyway.
Finally, if it was considered really valuable, then someone could implement the same implicit adoption that occurs when SAL attributes are present, as already occurs if you do something like
void f(int N, int buffer[N])
while retaining the significantly more powerful and flexible attributes provided by this RFC.If this patent is genuinely enforceable, surely the names of the keywords aren't relevant?
struct TerribleArray {
int count;
int *elements;
}
correctly bounds check that elements was at least count elements long? and that indexing into elements is restricted to 0..<count?This will fail like all the other annotation attempts have failed.
I like it and it is on my list to implement as a prototype for GCC.
And WG14 takes security seriously, but I think you overestimate the power WG14 has to simply change things. WG14 is supposed to standardize existing practice, not reinvent the language. So you should complain to compiler vendors. Or contribute to the development of open-source compilers.
Plenty of compiler vendors have extensions for safer C. Microsoft introduced similar annotation mechanism like the one being discussed here when Windows XP SP 2 came to be, in 2001.
Apparently 50 years weren't enough to make it happen.
Contrast this with how WG21 looks into security and code safety, enough papers on the subject, specially after the cybersecurity bills started to come about.
Without this code execution would just continue to whatever function is in memory, if there’s one.
And yet… it’s still considered c language.
[1] C17 standard 4.5