I don’t think the spec says one way or another (but please correct me if you find verbiage indicating that the language must be memory unsafe).
It’s possible to make the whole language memory safe, including unions. It’s tricky, but possible.
Someone else mentioned Fil-C but Fil-C builds on a lot of prior art. The fact that C and C++ can be memory safe is no secret to those who understand language implementation.
But yes, fil-c is a huge improvement (afaik though it doesn’t solve the UB problem - it just guarantees you can’t have a memory safety issue as a result)
This statement doesn't make sense to me.
Memory safety is a property of language implementations, which is all about what happens when the programmer does not follow the rules.
> The problem is that the rules cannot be automatically checked and in practice are the source of unenumerable issues from straight up bugs to subtle standards violations that trigger the optimizer to rewrite your code into what you didn’t intend.
They can be automatically checked and Fil-C proves this. The prior art had already proved it before Fil-C existed.
> But yes, fil-c is a huge improvement (afaik though it doesn’t solve the UB problem - it just guarantees you can’t have a memory safety issue as a result)
Fil-C doesn't have UB. If you find anything that looks like UB to you, please file a GH issue.
Let's also be clear that you're referring to nasal demons specifically, not UB generally. In some contexts, like CPU ISAs, UB means a trap, rather than nasal demons. So let's use the term "nasal demons".
C and C++ only have nasal demons because:
- Policy decisions. For example, making signed integer addition have nasal demons is because someone wanted to cook a benchmark.
- Lack of memory safety in most implementations, combined with a refusal to acknowledge what happens when the wrong kind of memory access occurs. (Note that CPU ISAs like x86 and ARM are not memory safe, but have no nasal demons, because they do define what happens when any kind of memory access occurs.)
So anyway, Fil-C has no nasal demons, because:
- I turned off all of those silly policy decisions for cooking benchmarks.
- The memory safety means that I define what happens when the wrong kind of memory access occurs: the program gets killed with a panic.
That’s good to know about nasal demons. Are you saying you somehow inhibit the optimizer from injecting a security vulnerability due to UB ala https://www.cve.org/CVERecord?id=CVE-2009-1897 ? I’m kinda curious how you trick LLVM into not optimizing through UB since it’s UB model is so tuned to the C/C++ standard.
Anyway, Fil-C is only currently working on (a lot of, but not all yet I think right?) Linux userspace while C and C++ as a standard language definition span a lot more environments. I agree the website should call out Fil-C as memory safe but I think it’s also fair to say that Fil-C is more an independent dialect of C/C++ (eg you do have to patch some existing software) - IMHO it’s too confusing for communicating out to say that C/C++ is memory safe and I’d rather it say something like Fil-C is memory safe or C/C++ code running under Fil-C is memory safe.
> Memory safety is a property of language implementations, which is all about what happens when the programmer does not follow the rules.
By this argument no language is memory safe because every language has bugs that can result in memory safety issues. Certainly rustc definitely has soundness issues that haven’t been fixed and I believe this is also true of Python, JavaScript, etc but I think it’s an unhelpful bar or framing of the problem. The language itself is memory safe and any safety issues within the language spec or implementation are a bug to be fixed. That isn’t true of C/C++ where there’s going to always exist environments where it’s impossible to even have a memory safe implementation (eg microcontrollers) let alone mandate one in the spec. And also fil-C does have a performance impact so some software may not ever be a good fit for it (eg video encoders/decoders). For example, a non memory safe conforming implementation of JavaScript is not possible. Same goes for safe rust, Python or Java. By comparison that isn’t true for c/c++.
Reasonable people will disagree about what memory safety (and type safety) mean to them. Personally, bounds checking for arrays and strings, some solution for safe deallocation of memory, and an obviously correct way to write manual bounds checks is more interesting than (for example) no access to machine addresses and no FFI.
Regarding bounds checking, GNAT offers some interesting (non-standard) options: https://gcc.gnu.org/onlinedocs/gnat_ugn/Management-of-Overfl... Basically, you can write a bounds check in the most natural way, and the compiler will evaluate the check with infinite precision (or almost, to improve performance). In standard, you might end up with an exception in some corner cases where the check should pass. I wish more languages would offer something like this. Among widely used languages, only Python offers this capability because it uses infinite-precision integers.
Yes that is inhibited. There’s no trick. LLVM (and other compilers) choose to do those stupid things by policy, and the policy can be turned off. It’s not even hard to do it.
> Fil-C is more an independent dialect of C/C++ (eg you do have to patch some existing software)
Fil-C is not a dialect. The patches are similar to what you’d have to do if you were porting a C program to a new CPU architecture or a different compiler.
> By this argument no language is memory safe because every language has bugs that can result in memory safety issues.
You rebutted this argument for me:
> any safety issues within the language spec or implementation are a bug to be fixed
Exactly this. A memory safe language implementation treats outstanding memory safety issues as a bug to be fixed.
This is what makes almost all JS implementations, and Fil-C, memory safe.
Now, certain implementations of C might give your more guarantees for some (or all) of the behavior that the standard says is undefined. Fil-C is an example of an implementation taking this to the extreme. But it's not what is meant when one just says "C." Otherwise I would be able to compile my C code with any of my standard-compliant compilers and get a memory-safe executable, which is definitely not the case.
My meager understanding of unions is that they allow data of different types to be overlayed in the same area of memory, with the typical use case being for data structures that may contain different types of data (and the union typically being embedded in a struct that identifies the data type). This certainly presents problems with the interpretation of data stored in the union, but it also strikes me that the union object would have a clearly defined sized and the compiler would be able to flag any memory accesses outside of the bounds of the union. While this is clearly problematic, especially if at least one of the elements is a pointer, it also seems like the sort of problem that a compiler can catch (which is the benefit of Rust on this front).
Please correct me if I'm wrong. This sort of software development is a hobby for me (anything that I do for work is done in languages like Python).
Rust avoids this by having sum types, as well as preventing the user from constructing a tag that’s inconsistent with the union member. So it’s not that a union is inherent unsafe, but that the language’s design needs to control the construction and invariants of a union.
union {
char* p;
long i;
};
Then say that the attacker can write arbitrary integers into `i` and then trigger dereferences on `p`.The union example is not particularly problematic in this regard. Much more challenging is pointer arithmetic through uintptr_t because it's quite common. It's probably still solvable, but at a certain point, changes the sources becomes easier, even at at scale (say if something uses the %p format specifier with sprintf/sscanf).
Real C programs use these kinds of unions and real C compilers ascribe bitcast semantics to this union. LLVM has a lot of heavy machinery to make sure that the programmer gets exactly what then expected here.
The spec is brain damage. You should ignore it if you want to be able to reason about C.
> This is not just hypothetical: existing implementations with pointer capabilities (Fil-C, CHERI targets, possibly even compilers for IBM i) already do this
Fil-C does not abort when you use this union. You get memory safe semantics:
- you can use `i` to change the pointer’s intval. But the capability can’t be changed that way. So if you make a mistake you’ll end up with an OOB pointer.
- you can use `i` to read the pointer’s current intval just as if you had done an ptrtoint cast.
I think CHERI also does not abort on the union itself. I think storing to `i` removes the capability bit so `p` crashes on deref.
> The union example is not particularly problematic in this regard. Much more challenging is pointer arithmetic through uintptr_t because it's quite common.
The union problem is one of the reasons why C is not memory safe, because C compilers give unions the expected structured assembly semantics, not whatever nonsense is in the spec.