Checked C
github.com
github.com
Microsoft: "Rather than changing the run-time representation of a pointer in order to support bounds checking, in Checked C the programmer associates abounds expression with each_Array_ptr<T>-typed variable and member to indicate where the bounds are stored. The compiler inserts a run-time check that ensures that deferencing an_Array_ptr<T>is safe (the compiler may optimize away the runtime check if it can prove it always passes). Bounds expressions consist of non-modifying C expressions and can involve variables, parameters, and struct field members. For bounds on members, the bounds can refer only to other members declared in the same structure."
Me: No array descriptors are generated by the compiler. The programmer tells the compiler the size of the array as an expression, and that expression is evaluated at the point in the program where the relevant declaration appears.
The Microsoft syntax is very Microsoft. It involves many leading underscores and long keywords. What I proposed involved adding C++ "&" references, and making lengths explicit in the places in C where you can write "[]". This is more concise. The basic idea is the same, though. Arrays have a length, which is defined by some expression on nearby variables, such as other fields of the same struct or other parameters in the same function call. C programs that work usually have a length around somewhere, but the compiler doesn't know where it is. Make that explicit, and you can check.
If you add ranges and references to C syntax, and provide length information, you can deal with one of C's three big questions - "How big is it?" (The other two big questions are "who owns it?" and "what locks it?")
This area is 10% technology and 90% politics, so you need sizable organizational support to make much happen.
[1] http://www.animats.com/papers/languages/safearraysforc43.pdf
I didn't understand this part:
int arr100[100]; // an array of 100 ints
arr100& arr100r; // ref to array of 100 ints
> This is valid C++ today.
Is it really? That doesn't look valid to me, but I'm well and truly much more of a C programmer than a C++ one. I did try it on Godbolt, and got an error: <source>:2:1: error: 'arr100' does not name a type
2 | arr100& arr100r; // ref to array of 100 ints
| ^~~~~~
Compiler returned: 1
Apologies if this is already mentioned somewhere in errata or something, or if I'm simply being too dense. decltype(arr100)& arr100r = arr100;
or just auto& arr100r = arr100;Not at all, Microsoft follows the ISO C guidelines to add new keywords to the language.
Which is why you have stuff like _Generic, _Atomic,....
Do you really think the "C community" as a whole will accept ANY syntax changes to the language?
From my experience I would say no. I think it would be "easier" to force tools on them than any syntax change (even if really really minor).
And you can get Annex K, on Windows.
On the other hand, exactly because of the way some C developers are against anything that helps security, is why Microsoft, Google, Oracle, ARM, Apple are all pursuing projects to have hardware memory tagging.
On Solaris SPARC, it already doesn't matter what those more religious developers think about bounds checking and similar issues.
No. That's why I didn't push on this.
I have some disagreements with the design of Rust, but it's a big step forward.
Mind you, Ada/SPARK has been out there for quite some time now, and it has been a "big step forward" of what people were unaware, unfortunately. Only if Ada/SPARK got the hype of Rust... there would not be much of a need for Rust. Ada/SPARK would have an ideal ecosystem, perhaps even another compiler, and so on.
They also have SAL annotations for "what locks it?".
• C is valued for being super portable and compatible with every platform, including legacy ones, but this applies to specifically C89 or C90, and nothing else. As soon as you embrace new features, it's not your universal C any more. You're going to need newer compilers, new tooling, and that's almost like starting with a new language.
• C users have mostly self-selected themselves to like C the way it is: a small, flexible, zero-overhead language that isn't C++. Addition of checked containers, generics, and restrictions on pointer arithmetic, etc. make checked C not feel like C any more.
• Checked C still needs refactoring of the code. For large established codebases this is still a lot of work, and still a risk of introducing new bugs. It's very tempting to settle on just running one more analyzer, sanitizer or fuzzer instead.
And in the end with all this effort, back-compat break, whole-codebase refactoring you're still left with the old C, with headers and include paths, sloppy macros, ancient stdlib, painful dependency management, etc.
If you're going to rewrite C incrementally to a more complex dialect, and require a newer non-standard compiler for it, you may as well run c2rust on it first.
I do not see what would make C more flexible than any other language. The other points are correct and C's niche is indeed that it is small, ubiquitous, simple, and yet low level.
One reference implementation is a lock-in, not only technologically, but also socially in terms of a community that is too monolithic for its own good.
That's not true for all those examples: both Ruby and Python have several great implementations and Go started with specification and two implementations.
Let's not forget that C is ~50 years old and yet having reliable, open-source, high-quality compilers is a thing of the past 20 years or so. Clang was born in the 2000's, GCC is much older but it wasn't that great of a compiler for some time. Same with VS.
It might be a lock-in but fragmentation is bad too.
An unsuccessful attempt at embrace, extend, extinguish.
If they just needed type checking, it could've been done with either tests, or annotation for checks at compile time, but instead they've developed a whole new language that gets more, and more diverged from the original one with each release.
Also "TypeScript thing" is definitely downplaying where TS is as a language https://2020.stateofjs.com/en-US/technologies/javascript-fla...
Many of the people that keep using C as is, will never use something else no matter what.
And indeed, it's probably not suitable for refactoring, but if you want to start a driver from scratch?
https://www.digitalmars.com/articles/C-biggest-mistake.html
i.e. just change:
void foo(char* a)
to: void foo(char[] a)
and that passes a phat pointer (pointer and length) instead of just a pointer, enabling array bounds checking. void foo(char a[..])
for passing an array pointer and length, and to eventually deprecate: void foo(char a[])
Personally I like this suggestion, but as the article is 12 years old, I'm obviously in a minority. void foo(char[] a)
instead of what he originally proposed, which is: void foo(char a[..])
If so, that does seem cleaner. I still like the overall suggestion.Which is amazing. It's a simple, backwards compatible solution to buffer overflows. I know it works because it's been used in D for 20 years and has made buffer overflows a thing of the past.
It's also nice because it implicitly documents the difference between a pointer to a single object and a pointer to an array of objects. Just this is a huge win, when used consistently.
It's completely compatible if you use the [..] syntax.
> performance/size penalties
For the proposed scheme, they are essentially non-existent. This is because the array bounds is usually already being passed as a separate argument.
Arrays that rely on a sentinel to determine the end (like 0-terminated strings) one can just not use the new syntax.
I'm still not convinced the new languages (Rust & Zig) will change the overall situation.
Why? Simple example in Rust:
"LOL! I need to get stuff done."
unsafe {
Really nasty hacks here to circumvent any obstacles.
}
Example in Zig: "LOL! I need to get stuff done. Tests? What tests???"
zig build-exe example.zig -O ReleaseFast
Trying to solve those security bugs with the help of the language is of course the low hanging fruit. But systems languages will always need an escape hatch for certain problems to be solved.
And those mechanisms WILL be abused in the name of "get it done already".So we really need other means to erase those bug classes. Which ones I don't know.
I'm not sure I follow, it seems to me like it's actually a freaking high-hanging fruit. The amount of engineering and design that goes into creating a safe, fast, low-level programming language (i.e. Rust in this case) is enormous and it requires huge amounts of upfront investment.
The only reason the industry has largely resorted to unsafe languages is because there's many other lower-hanging fruits, e.g. fuzzing, rigorous unit testing, static analysis tools. These help immensely but at the end of the day only take you so far.
Will they do things in that way? Heck, let’s just ask: are they doing things in those ways?
crickets
Just meaningless rhetoric (“being done fast and/or cheaply”) and unfounded hypotheticals.
I do not believe this. Most C code by far is not written in a way that makes it easy to formally guarantee memory safety, so the lowest effort path will necessarily involve refactoring it until it does.
And that means that rewriting it altogether into a new language has very few drawbacks, and some additional benefits.
No, because while some refactoring is necessary, it is virtually always local. E.g. if the sound checker can't prove that an access is within an arrays bounds, adding a dynamic test just before it (assuming you don't want to add annotations to help the checker) will make it provable.
Rewriting in a new language is certainly one way to reduce or eliminate memory errors, but if your goal is to reduce or eliminate memory errors in existing codebases, there are other ways that are cheaper, more proven, less risky and are applicable to more environments. These tools are already much more heavily used in safety- and security-critical codebases than any new language, so people are slowly getting good experience with them.
Yes, you can rewrite everything in Elm, PureScript or even anything that is more similar to JS, but step by step conversion to TS enables a significant quality improvement for large codebases (I personally think it doesn't fare so well on small codebases, where there are some people who use it even for one-off scripts, but at least we can agree on large codebases I think)
I'm not sure that's what you meant, but interoperability between C and Rust does seem to be a selling point of Rust (like TS and JS), and allows C codebases to be oxidized gradually rather than rewritten from scratch.
By the way, I love rust, but I just don't understand the "rewrite all the things" hype. Being battle tested means a lot for low-level libs. You can't guarantee that you ported everything exactly, with bugs and all. Pragmatic solutions like "Checked C" prove a lot in that regard.
You actually can “just” turn C into Rust with something like c2rust. But you haven't gained any safety without refactoring.
Is that not also the case for Checked C?
Outside Bell Labs, operating systems were being written in safer systems programming languages, and one of the reasons why Unisys still has enough customers that care to pay for ClearPath MCP, which has a security level no UNIX clone is capable of, thanks NEWP.
The interesting thing about C is that it is one of the few languages which has been able to survive without massive changes throughout the years. All these hyped-up modern languages are in flux ALL THE TIME.
Ah, it’s simple, then. We will await eagerly for the non-forthcoming links to all those efforts/implementations.
Or would you like to walk that back to “in principle suitable for automated detection”?
Although I wouldn't mind if Pascal or Ada swings back to mainstream's attention.
As per wiki, they state that they have automatic conversion tools, but I highly doubt that this will be used in production, even in Microsoft.
Example from their wiki (https://github.com/microsoft/checkedc/blob/master/samples/st...):
// Delete all c from p (adapted from p. 47, K&R 2nd Edition)
// p implicitly has count(0).
checked void squeeze(nt_array_ptr<char> p, char c) {
int i = 0, j = 0;
// Create a temporary whose count of elements can
// change.
nt_array_ptr<char> s : count(i) = p;
for ( ; s[i] != '\0'; i++) {
// We need to widen the bounds of s so that we
// can assign to s[j] when j == i.
// (In the long run, this will be taken care of by the
// flow-sensitive bounds widening).
nt_array_ptr<char> tmp : count(i + 1) = s;
if (tmp[i] != c)
tmp[j++] = tmp[i];
}
// if i==j, this writes a 0 at the upper bound. Writing a 0 at the upper bound
// is allowed for pointers to null-terminated arrays. It is not allowed for
// regular arrays.
s[j] = 0;
}For a simple example, if you have a struct that has a pointer to an array that is allocated previously and that also has a length and maybe an allocated amount (and you're careful with making sure they are correct), then you can pass a pointer to the struct to a function that verifies that the length is sufficient for what is to be done and then the function doesn't have to check the length every time it accesses something in the array if it can be assured that the allocated memory or the length didn't change. This could save a lot of generic behind-the-scenes code that verifies the length every time an array element is accessed. It's up to the developer to make sure all the pieces are in place and everything is prepared before calling this function. If that code is in a tight loop, it could make a big difference in performance.
It's a sharp knife, but sometimes you need a sharp knife. You just have to be more careful. Dulling the blade makes it more safe, but it doesn't let you make use of it's sharpness.
Is it often overused? Probably. Does it have a steep learning curve to be able to write code that doesn't exhibit certain classes of errors? Sure. Should other options be used where performance isn't as critical? Probably. But it has it's place.
There is also cyclone.
Finally, if you missed it there is a new probabilistic variant of asan: https://llvm.org/docs/GwpAsan.html (and also a hardware accelerates one but only for ARM)
You can even implement intrusive collections: https://docs.rs/intrusive-collections/
Unsafe is a normal part of the language, and how you manually implement precise control behind a safe reusable abstraction whose correct use can be verified by the compiler.
Do you have any examples of precise control you can express in C, but can't express in Rust?
And? That's what unsafe is for.
It might not matter at all for what is being developed, but in no way are you going to spend those extra 5ms required for safer code.
#include <stdio_checked.h>
#include <stdchecked.h>
#pragma CHECKED_SCOPE ON
int main(int argc, nt_array_ptr<char> argv checked[] : count(argc)) {
puts("hello, world");
return 0;
}https://support.apple.com/guide/security/memory-safe-iboot-i...
Apple writes every instruction executed by the processor from the moment it powers on until it’s booted. Unlike for instance devices running on the Intel platform, where the processor is running an entire OS behind your back, and you can’t turn it off.
I get downvoted a lot for saying this, but I'll say it anyway: I'm getting increasingly convinced that the purpose of this whole "safe languages" push is ultimately to the benefit of companies who want to deepen and their control over users. If you look at who the biggest proponents are, it's pretty obvious what their motivations are. They don't want you jailbreaking or rooting, they want to force you into their walled garden and keep you there.
Likewise, I usually think of Mozilla as one of the primary promoters of safe languages (specifically, Rust). Do you think they benefit in some illicit way if someone chooses to use Rust?
I read your linked post, but, without reading the whole thread, I'm not sure how it doesn't amount to saying that security bugs are, on net, a good thing.
To be clear, I have my own criticisms of these newer languages that center on their corporate-driven development (e.g. they're generally designed around the belief that the developer should also be the final packager of the software or should at least determine what the final binary looks like), but their additional security is not something that I had thought to criticize as such.
Do I want a browser that's free of remotely exploitable vulnerabilities? Yes.
Do I want a "device" that I can't take control of, despite the fact that I bought it? Do I want unbreakable DRM? No. Hell no.
The problem is that those who work on the latter will see the former, and adopt the same technologies to enslave instead of empower.
Ultimately it's a balance of power. Neither total lawlessness nor total authoritarianism ("zero crime", as some like to call it), to use a real-world analogy.
I'm a very experienced C programmer and what I would like, much more then a new language version, are some basic OS functions where I can query the status of a pointer. Is it readable? Is it writable? Where does the memory allocation start and end? Is the memory in the heap or stack, or a memory mapped file?
All the information is available inside the OS, but I cant get at it. If it was available it would be possible to write all kinds of instrumentation. If it was possible (in debug mode) to get the layout of the stack, that would also be super useful. Buffer overruns are really mostly a problem when they are on the stack.
By warping malloc and free, you can easily find buffer over runs, memory leaks and double frees. They become a none issue. But with stack memory you have very little control. I avoid using the stack for operations where this might be an issue, just because its so much harder to debug.
I would love to be able to write APIs that have built in tests (in debug mode) where all pointers are checked, and if anything is broken the API can fail gracefully and give out precise errors.
Lets say you have an API that implements a function similar to memcpy. It interacts with more then one user provided pointer. Lets also assume that the API is something big and complicated like a graphics driver, possibly closed source.
If you give it a broken pointer it may crash, but it wont tell you what pointer was bad and in what way. Maybe no pointer was bad, maybe the implementation is broken, or some other param is broken. An API can check other params to make sure they are valid, and many APIs do, bu its not yet possible to check poiners.
You have to dig in to the implementation to do that and that's a lot of work even if its opensource. Crashing in your code is great, because the debugger will tell you what is going on, but crashing in someone elses code is much harder to debug.
Worse, the code may not crash at all, it may over write another allocation by the same process. The implementation cant check the mallocs, it only crashes if the pointer steps outside of a memory block owned by the process.
An error message is much easier to read then a stacktrace, especially if you dont have source-code.
> If you want to do what you describe you have to make such changes that the language really isn’t c anymore.
I dont want any changes to C. I just want to be able to query a process allocations. the OS has to keep the information in order to implement realloc and free, so the information is already there.
Applications use malloc to allocate some memory they then use to store a collection of data. Pointers often don’t point to the exact start of that memory area, which is what you need to do memory management. So from a pointer you can’t necessarily realloc or free.
And the problem is, if a buffer overruns for instance, you end up writing memory that is allocated and valid, just not for the thing the developer thought.
Alternative implementations of malloc exist that try to help with this, by encasing memory between guard pages.
Also, with for example glibc malloc you can use the MALLOC_TRACE environment variable to get a log of all allocations.
https://docs.microsoft.com/en-us/cpp/c-runtime-library/refer...
I don't think the OS should be in charge of memory safety in this way since this can all be done in user space.
http://gamepipeline.org/forge_Debugging_.html
Most of why i want this kind of functionality is not to test my code but to be able to build tools. I have written a live memory viewer/poker. Memory query would really help that kind of effort:
So, a weakly typed language with garbage collection? Where have we heard of that before?
I can imagine that if most of your code is for checking that the remaining fraction reads correctly from rvalues and writes correctly to lvalues and each check requires a round trip through a context switch, you're not going to be wringing very much productivity out of either your hardware as the CPU thrashes like mad or the one developer you manage to keep on staff.
Performance would not need to be great, I would even assume it would be quite bad. But being able to write APIs, that have debug mode that tells you precisely when you give a API call broken pointers would be very useful.
VirtualQuery but the granularity is coarse, pages are 4kb.
> Where does the memory allocation start and end?
HeapSize if that's on heap, and you know the heap.
> Is the memory in the heap or stack, or a memory mapped file?
QueryVirtualMemoryInformation
> APIs that have built in tests (in debug mode) where all pointers are checked, and if anything is broken the API can fail gracefully and give out precise errors.
Visual C++ in debug builds does lots of runtime checks on all levels, C runtime, C++ runtime, and even compiler. For instance, they fill uninitialized and freed memory with magic numbers.
HeapSize is close, but cant be used to check incoming pointers since it has to be the start of the allocation, and again it doesn't recognize the stack.
VirtualQuery is better but as you say a little course.