Loading Stories...
Loading Stories...
int main(){
int x;
if (x <= 42){
assert(x != 12345);
}
}
is of course UB in C, even though under a "uninitialized is random" model the program is valid and does not assert (as the model checker concludes).(even in O1 clang gets rid of the whole function, including even the ret instruction, I'm surprised it does not at least leave an ud2 for an empty function to help debugging since it would not cost anything https://godbolt.org/z/eK8cz3EPe )
I gave a talk about Frama-C a few years ago. It's very interesting technology, but not too useful at any scale (http://oirase.annexia.org/tmp/2020-frama-c-tech-talk.mp4) One aim we had with Bounds Checking GCC back in '95 was to make it possible to use with real programs. Although it was quite slow, any open source program of the era could use it (https://www.doc.ic.ac.uk/~phjk/BoundsChecking.html).
int x;
if (x < 42) { assert (x != 12345); }
isn't to have a checker is clever enough to know that the assert will not go off, but to be informed that the automatic variable x is being accessed without being initialized!!! int main(){
char buffer[10];
buffer[10] = 0;
}
are so rare they are hardly worth bothering with. The more usual case is: int get(char* buffer)
{
return buffer[10];
}
void test() {
char buffer[10];
get(buffer);
}
I.e. the array bounds for buffer get lost in the function call. I have proposed a fix:https://www.digitalmars.com/articles/C-biggest-mistake.html
that is compatible with existing code.
And yet, nobody cares. Oh well! Instead, we have overly complex solutions like this:
https://developers.redhat.com/articles/2022/09/17/gccs-new-f...
No, it does not. It probably does constraint based verification or looks for "proofs" of possible error conditions or asserts. In the case shown, an uninitialized variable is trivial to prove that it could equal the asseted value.
Not that I'm an expert in processing what exactly is being tested, but it basically looks only able to prove that an individual function doesn't overrun buffers. If you tell it to assume that integer overflows can't happen (!). So I'm not impressed.
Intelligence is knowing that the assert will never go off.
Wisdom is knowing that it might.
[1]: https://thephd.dev/_vendor/future_cxx/papers/C%20-%20Non-own...
extern int nondet (void);
int x = nondet();
if (x < 42) { assert (x != 12345); }
BTW, it's simple to implement.
T f(args)[..] { body }
This would be hard to parse. Then what about: T[..] f(args) { body }
But if adding [] after T is allowed in function declaration, then why not for arrays as well: T[..] a;
So the checker treats them as defined, but with an unknown variable. You could have written this instead:
extern int unknown_int_value(void);
int x = unknown_int_value();
And leaving unknown_int_value undefined (so it's not visible to the analyzer). Or write a function and use x as a parameter.I suspect CBMC does this to have a convenient syntax for this frequent scenario. Apparently, it's used quite often, as in these examples: https://model-checking.github.io/cbmc-training/cbmc/overview...
It seems that CBMC is not intended to check production sources directly against C semantics, but to prove things about programs written in a C-like syntax.
Any invocation of undefined behavior should be considered an assertion failure as far as any model checker should be concerned. Compilers can--and will--treat undefined behavior as license to alter the semantics of your program without any constraint, and most instances of undefined behavior are clearly programmer error (there is no good reason to read uninitialized memory, for example).
Reading an uninitialized variable is not "subtle" undefined behavior. It's one of the most readily accessible examples not only of what undefined behavior can exist, but also the ways compilers will mutilate your code just because you did it. To be honest, if something as simple as the consequences of reading uninitialized memory are shaky understanding for someone trying to prove code correct, that will completely undermine any trust I have in the validity of your proofs.
Your operating system likely feels entitled to assume that if you never wrote to this page of RAM you don't care what exactly is in it. After all what kind of lunatic reads a bunch of unknown data, says "Yeah, that's coincidentally what I wanted" and just leaves it unmodified? No, almost anybody would write data they want to keep instead. So, if you never wrote to this particular page of RAM and your OS finds it convenient to swap that page for a different one, no harm no foul right? But now the contents of your uninitialized variable changed!
The C standard text is barely sufficient for the purposes of compilers, but wholly insufficient for what a model checker would need to know.
That's an assumption on your part.
One very good reason might be to ensure that memory really is there given the optimistic allocation schemes and to force a page to be read into core. You could also write to it to get the same effect. Another good reason to do a read on uninitialized memory is to see what's still there from the previous program that ran. Now arguably that's a nefarious reason but it is still a good one and it may help uncover bugs in the underlying system.
Of course the toy example is a toy, and of course it does not "run" anything, but this is a sound technique: if there is an execution that violates a property, it will be found.
I highly suggest reading the paper that introduced it [1], it is remarkably clear!
[1] Edmund Clarke, Daniel Kroening, and Flavio Lerda. 2004. A Tool for Checking ANSI-C Programs. In 10th International Conference on Tools and Algorithms for the Construction and Analysis of Systems (TACAS) (LNCS), 2004, Barcelona, Spain. Springer, 168–176. https://doi.org/10.1007/978-3-540-24730-2_15
In general, the things your compiler thinks are UB are not the same things your OS or CPU thinks are undefined.
This isn't perfect, of course. In this case, the compiler can rightly treat x as uninitialized, meaning that its value could be <= 42 at one point, and not be <= 42 at another point. Since the uninitialized variable isn't "pinned", it could technically be different values in different locations.
CBMC's machine model works differently. In this case, x is assigned a non-deterministic value. The branch condition creates a refinement of this value within the scope of that statement. If the branch is taken, then by SMT rules, x can't equal 12345, because it was already refined as being <= 42.
On its own, a model checker can miss situations like these. It's why I recommend -Wall -Werror -Wpedantic in conjunction with CBMC. The compiler should catch this as a warning, and it should be upgraded as an error.
And yes, it does happen in practice, most famously it has been mentioned in the "The strange details of std::string at Facebook" talk at CppCon 2016 https://youtu.be/kPR8h4-qZdk?t=1150&si=2R358wniZfxTJLmc
In hardware design, verification is done simultaneously with design, and semiconductor companies would be bankrupt if they did not verify the hell out of their designs before committing millions in manufacturing these chips.
Even among hobbyists this is getting traction with yosys.perhaps its time for programmers to adapt this kind of tooling so there will be less buggy software released...
Edit: nevermind, I mixed up MADV_DONTNEED with MADV_FREE
Depends on how you initialize them. If you do it all in one go, then yes. If you partially initialize it, then no: some of the padding bytes are guaranteed to be zero. (Observing this and maintaining this variant is an exercise for the reader.)
CBMC works best with a contract-based programming style, where contracts are enforced by assertions and shadow functions exist to simplify model checking.
A single reply on HN is hardly a place where idiomatic styles can be expounded, but it is quite possible to build a resource-oriented idiomatic style that is reinforced with CBMC.
int get(int n, char (*buffer)[n]) { return buffer[10]; }
int main() { char buffer[10]; get(10, &buffer); }
Then a run-time bounds checker or a model checker can find the bug: https://godbolt.org/z/a4ExKMn1s
cbmc /tmp/walter.c --bounds-check --pointer-check --function test
...
[get.pointer_dereference.5] line 3 dereference failure: pointer outside object bounds in buffer[(signed long int)10]: FAILURE
...
There are many footguns. People love their guns. Makes them feel powerful.Well, duh: if you remove the most egregious footguns from C, you end up basically with Pascal, and here is Why Pascal Is Not My Favorite Programming Language.
And when you try to explain to them that C's abstract machine has semantics of "when you omit a safety check, you actually makes a promise to the compiler that this check is redundant, that you'll promise that you'll make sure in some other way that in no possible execution will the illegal values ever show up in this place, and the compiler will actually hold you to this promise" — they throw tantrums and try to argue that no, it hasn't this semantics, and even if has, it's not the semantics as originally intended by K&R and it is the authors of the C compilers who are wrong (even though the authors of C compilers are the people who actually write both the C standards and the C compilers...)
Must have to do something with imprintment: I've learned C as my 3rd language, and from the start I was taught that its UB is absolutely insane — and I've never felt "nah, it should just do what I meant, dangit" denial about it, only "this is sad, how can I even know while writing code when I am making a baseless promise especially when it's made by omitting something?" But like you've said, people like their footguns.
- UB sanitizer https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
- Cerberus Semantics https://www.cl.cam.ac.uk/~pes20/cerberus/
What matters is if enough people in WG14 with voting rights care.
When the variable definition and its use are together in the same basic block, it's immediately obvious that the variable has a next-use on entry into the block, and that its definition hasn't supplied it with a value.
From there it gets complicated. GCC has a history of emitting overly zealous diagnostics in this area, warning about possibly uninitialized variables that can be proven not to be.
The difficulty is equivalent to the halting problem:
void f()
{
int x;
if (invokes_undefined_behavior(f))
x = 'A';
putchar(x);
}
Which anyway was not an issue in the PL/I variants that predated C, nor the Mesa/Modula-2 descendents from the mid-70's.
EDIT:
Walter's example with 1978's Modula-2.
MODULE Example;
FROM InOut IMPORT WriteString, Write, WriteLn;
PROCEDURE get(buffer : ARRAY OF CHAR): CHAR;
BEGIN
RETURN buffer[10]; (* This will boom unless compiled with bounds checking disabled, in which case who knows *)
END get;
VAR
buffer : ARRAY [0..9] OF CHAR;
BEGIN
WriteString('The out of bounds character is');
Write(get(buffer));
WriteLn;
END Example.
No sane OS will do this. Any page that's handed to a process that was last written by a different process must be zero'd (or otherwise have every address initialized) by the OS to avoid leaking information across process boundaries. You could, in theory, have a page that was munmap'd by /this/ process be handed back to the same process to fill a request for a different virtual address without zeroing it, but I can't imagine that any OS tracks the last writer to enable this "optimization" in the few cases it would apply.
int get(char* buffer, int i)
{
return buffer[i];
}
#include <stdio.h>
void test() {
char buffer[10];
get(buffer, getc(stdin));
}
sizeof(a)
sizeof(a)/sizeof(a[0])
I don't think accepting the random initial value means that the value may continue to change until the first time you set a value.
Eh, I think most C programmers frustrations with UB stem from knowing that the C standard has fundamental flaws and modern compilers abuse that fact for "optimizations" on UB. This paper covers the topic pretty well[0].
I fully support Casey Muratori's viewpoint that undefined behavior should not exist in the standard[1]. Instead, the C standard should enumerate all valid behaviors compliant compilers can implement. This would allow compilers to make unintuitive optimizations for the platforms that need them, but still allow programmers to be certain that their program semantics will not change in different versions of the said compiler.
[0] https://www.complang.tuwien.ac.at/kps2015/proceedings/KPS_20... [1] https://youtu.be/dyI0CwK386E?si=vsqJ8uWHY8xkGmFm
The reasoning about nondeterministic values should be spared for situations when it's other than undefined behavior. For instance, accessing an uninitialized structure member that came from malloc isn't undefined behavior. It's (I think) unspecified behavior. In an implementation that has trap representations for some types, it could hit a trap.
int get(int n, char buf[n])
{
return buf[10];
}
It's if you want to use sizeof that you need a pointer to array. But if you have n, why would you need to query the size of your array?Weird things happen to uninitialized variables during optimization. Basically, nothing can be assumed about the value, because using an uninitialized variable results in undefined behavior. The original un-optimized generated code may very well have some default value, maybe. Probably not. But, the optimizer can decide that this value is represented by register R0 here and R4 there, and since the variable is uninitialized, there's no point in doing a spill or a load. It really comes down to how the optimization passes were written.
Practically speaking, -Wall -Werror should catch this. Any use of a tool like CBMC should be part of a defense in depth strategy for code safety.
It does in clang.
$ cc -Wall -Werror bar.c
bar.c:5:11: error: variable 'x' is uninitialized when used here [-Werror,-Wuninitialized]
if (x <= 42){
^
bar.c:4:12: note: initialize the variable 'x' to silence this warning
int x;
^
= 0
$ cc --version
clang version 16.0.6
It also does in gcc. $ gcc -Wall -Werror bar.c
bar.c: In function 'main':
bar.c:5:10: error: 'x' is used uninitialized [-Werror=uninitialized]
5 | if (x <= 42){
| ^
bar.c:4:11: note: 'x' was declared here
4 | int x;
| ^
cc1: all warnings being treated as errors
$ gcc --version
gcc 12.2.0
Do they actually know that? I don't think so. Let's take [0] for instance (the author is an expreienced C programmer who loves the language and wrote a re-implementation of bc in it):
There seemed to be a lot of misunderstandings; I could not get a handle on what this person thought UB meant.
I finally figured it out: this person’s definition of UB was not “the language spec can’t guarantee anything.” Instead, it was “compilers can assume UB does not exist and optimize accordingly.”
Wat.
Yep. Apparently, that was news to him, even though C implementations have been behaving like that for about 30 years already, and you can read the rest of the post for the "this is evil, we the users must do something about it" take. And the proposed "something" is not "we should instead use a language with actually defined semantics", oh no. It's "use compiler flags to force more reasonable behaviour, hopefully" and "somebody should write boringcc, unfortunately, I am myself a bit too busy for that". Well, despite numerous pleas and several attempts, nobody has managed to write boringcc which is telling of something, I'm just not sure of what exactly. So... I think it is about imprinting: "Oh, it's a wonderful language, well, it would be if it was actually implemented the way I used to think it is implemented (and I still think it should be implemented that way) but still, it's a wonderful language if only not for that pesky realitiy" is forcing unfounded expectations onto reality, and where do these expectations even come from in the first place?And yeah, I fully agree with you that C standard should've probably done that. But it didn't happen, and it can't happen because backwards compatibility [1]. But even then, C programs would still be non-portable, in a sense that you have to use #ifdef's for tinkering with platform-specific behaviour for anything interesting, because C standard even today leaves a lot of stuff completely up to implementation, see [2] for an especially apalling example, even without touching UB.
[0] https://gavinhoward.com/2023/08/the-scourge-of-00ub/
[1] https://thephd.dev/your-c-compiler-and-standard-library-will...
[2] https://thephd.dev/conformance-should-mean-something-fputc-a...
Whether or not this is a sane OS I leave an exercise to the reader, but it is nonetheless the property of a common OS.
LLVM has a "freeze" command to stop propagation of undefined values (although I think that command was added later than the first version), so that the value is "pinned" as you say. However, the "undef" command, if you do not use "freeze", will not do this.
I think that the C compiler should "pin" such undefined values where they are used, but I don't know which compilers have an option to do this. (Perhaps CBMC should also have such a switch, so that you can use the same options that you will use with the C compiler.)
With this ex.3 file, the optimizer should be allowed to result in a function that does nothing and has an unspecified return value (which might or might not be the same each time it is executed). (If optimizations are disabled, then it should actually compile the conditional branch instruction.)
But I was also talking about UB in the context of already existing code! My argument is that compiler writers are breaking existing code when they could very well avoid breaking it.
We have tried to convince compiler writers to not do this, but they have refused. So that's why I said that users must do something: because compiler writers won't.
And yes, I knew compilers would take every advantage that they could before that; my surprise was that someone considered UB's sole purpose to be for the benefit of compiler writers at the expense of everyone else, including non-programmers who suffer catastrophic consequences for security bugs.
For something I don't use?
How about this: I would suggest the CMBC developers read HN comments about their stuff, when it comes up.
John Regehr has a blog post that touches upon why this is the case [0]:
> I’ll assume you’re familiar with the Proposal for Friendly C and perhaps also Dan Bernstein’s recent call for a Boring C compiler. Both proposals are reactions to creeping exploitation of undefined behaviors as C/C++ compilers get better optimizers. In contrast, we want old code to just keep working, with latent bugs remaining latent.
> After publishing the Friendly C Proposal, I spent some time discussing its design with people, and eventually I came to the depressing conclusion that there’s no way to get a group of C experts — even if they are knowledgable, intelligent, and otherwise reasonable — to agree on the Friendly C dialect. There are just too many variations, each with its own set of performance tradeoffs, for consensus to be possible.
Granted, at the end he says that it's not that it's impossible, it's just not for him, so it's still possible that someone else would succeed, but they'd have an uphill battle for sure.
(there's also usually a write, unless the allocation-head-metadata is on a different page than the parts of the allocation you're acting spooky with)
Edit: after watching the video elsewhere in thread, it was indeed crossing page boundary, but that behavior has to be a kernel bug since the documentation says "accesses" are sufficient to repopulate it.
use of the variable as an lvalue is not undefined
So, it will find a counter-example.
> However, subsequent writes to pages in the range will succeed and then kernel cannot free those dirtied pages, so that the caller can always see just written data. If there is no subsequent write, the kernel can free the pages at any time. Once pages in the range have been freed, the caller will see zero-fill-on-demand pages upon subsequent page references.
The sensible way for developers to become aware of possible improvements they could make is for them to constantly scan the rest of the internet.
The programmer will have to add code to keep track of the length of the array, and add code to check the value of the index. The good thing is the analyzer will tell you where you'll need to add those checks.
With the proposal I made, none of this is necessary. It just works. Over two decades of experience with this approach in D is it-just-works.
This led to the creation of function contracts and shadow functions. When evaluating functions, we actually want any calls that it makes to go to shadow functions instead of real functions. When we write a function, we include a contract that includes anything it may return, any side-effects it may have, and any memory it may touch / allocate / initialize. We then write a twin function -- its shadow function -- that technically follows the same contract in the most simplified terms, randomly choosing whether to succeed or fail. From CBMC's perspective, it's equivalent enough for model checking. But, it removes additional stack depth or recursion.
A good example of this would be a shadow function for the POSIX read function. Its contract should verify that the descriptor it is passed is valid. It should also assert that the memory region it is passed is bounded by the size it is given. The shadow function picks a non-deterministic state based on how it should set errno and how it should return. This state follows the POSIX standard, but the underlying system calls don't matter. Likewise, depending on the return code, it should initialize a number of bytes in the read buffer with non-deterministic values.
I used this shadow read function to find a buffer overflow in networking code and also to find a DOS infinite loop error in a tagged binary file format reader we were using.
CBMC isn't perfect, but when coupled with a good linter and -Wall / -Werror / -Wpedantic, it is a very useful layer in a defense in depth strategy for safer software.
And I'm not talking about a default value, this doesn't imply a default value even though assigning an address will come with some value. But I do get that defining the name and type are a seperate thing that don't necessarily require assigning an address any more than assigning an a value.
The difference is, any default value would be arbitrary. 0 is just a value that has a meaning that the compiler can't know. It's almost as arbitrary as 37.
But picking the next available address to attach to the symbol is not like that.
The compiler always picks that itself and the programmer always has to ask what it is when they want to know it. The compiler can safely do that much right at declare time without making any assumptions. The value is still random, which is fine because 0 is almost the same as random, in that it's random if 0 would actually be the best default value for this particular item vs 1, -1, size, -size, size/2, etc.
Oh.
I feel like I should have more to say than that, but... oh.
Changing the n later has no impact on the size of the arrays later and the compiler is perfectly able to remember the original size.
I interpreted kazinator's original comment as essentially saying "This looks like something I might use myself, but for this dealbreaker issue."
The machine model tracks the array with its size and compares this to any index. This is loaded into an SMT solver. This is not a standard static analyzer. It's compiling the code to an abstract machine model and then running through this model with an SMT solver.
CBMC isn't perfect, but the strategies you are talking about are meant to defeat a linter or a shallow analyzer in a compiler. CBMC isn't a linter.
> The compiler always picks that itself
One of the best analogies I heard, years ago on IRC, is that good source code is like select cuts of meat and fat, and the compiler is like a meat grinder. What comes out of the meat grinder is hamburger. It bears little resemblance to the source code, syntax, or semantics. But, it is meat.
I'll talk about register machines with a stack, since code is generated on most architecture this way. But, that's not necessarily a thing that can be relied upon. The compiler will preserve "relevant meaning" once the optimization passes are finished. Side-effects and return values (if used by the caller in the case of LTO) may be preserved, but that's largely it. That being said, let's talk about what a variable is, from the perspective of a compiler. A variable is decomposed into a value, which may be referenced, may be changed, or may need to be preserved. The optimizer aggressively strips away anything that isn't immediately needed. If the compiler needs to preserve the value of the register, then it can choose several ways of doing this. If the value is fixed, then it may just become a constant in code. Fixed values can also be produced synthetically. If the value is variable that is set at runtime and referenced later, then it could be set in one register and transferred to another register. If neither is possible, then the value could be spilled to the stack. But, this spill location is not necessarily assigned to this variable. This spill location could be used for another variable later in the function when this one falls out of scope. Spill locations aren't necessarily fixed. On many ABIs, there are scratch registers, registers that must be preserved between calls (the called function must preserve them if used), and registers that can be overwritten after a call. How a value is preserved depends entirely on whether it must be preserved, and which is the most efficient way to do so.
If a variable in source code is referenced but never assigned, then the optimizer can optimize away any "storage" (i.e. stack or a register) for this variable. When the variable is "read", the value used is arbitrary, if at all. The "read" has no meaning and has been optimized away. The value isn't fixed to a random value either at compile time or at runtime. It's whatever flotsam happens to be at that particular place in code at that particular time. If the value is "read" once in one location and once again in another location, even on the next line of source code, these values could be the same or entirely different. There's no telling what the compiler will do, because the optimizer is really free to interpret this in any way. The only way to ensure that a value is preserved by the optimizer is to explicitly assign this variable a value in the source code. This assignment is respected, assuming that the variable is referenced afterward. Beyond that, we can't make any meaningful assumptions.
I'm not a user of CMBC. I read about it here, I wrote a comment here, and that's the end of it.
But for a minute I will bite. The submission has no link to the actual project. I found that by a web search. The project has no project trappings: no mailing list, or anything of the sort. The page says, "For questions about CBMC, contact Daniel Kroening". The name is a link to his home page. His home page has no obvious contact information. There are links to papers; probably his e-mail address is given in some of them. Let's click on "A Tool for Checking ANSI-C Programs". Hey, e-mail links in that page, before we even get to the PDF. But ... from 2004. Is k-----g@cs.cmu.edu from two decades ago going to work, given that he's at Oxford?
Suppose I hunt down his current e-mail address. What do I say? "Dear Sir, I feel really awkward to be contacting you about your CBMC program, which I have never actually used, but believe me when I say I have a valuable suggestion ..."
If I used the thing, I'd fix it myself and send a patch!
x = 42
y = w
x = 43
Search the web for "next-use information".I would not mail if it is only about unintialized variables.
SMT cannot solve cases where it simply cannot know what the length of an array is. For example, when array is allocated in code not available to the solver, or is set by the environment.
I remember trying out Spark a few years ago, which advertised its compile time checking abilities. I tried using an integer equation that used an OR, and it gave up on it.
If you need to add in range checking code to help the solver along, then the range check itself is a source of bugs, and the range limit has to be made available somewhere and be correct.
In my proposal, the array length is set at the time of creation of the array, and the length is carried along with the pointer. The solver's role then becomes one of eliminating the bounds check in the cases where it can prove the index is within range. The user doesn't have to do anything.
We've been using it in the implementation of the D compiler for a very long time now, and problems with buffer overflows are a faded memory.
P.S. I also added manual integer overflow checks when passing the size to malloc(), no matter how unlikely an overflow would be :-)
Sure it can. It sets the array length to a non-deterministic unsigned integer, and then finds a counter-example where this array length is invalid. CBMC will also make the array pointer itself non-deterministic if you haven't refined it. So, the pointer could be NULL, could be pointing to invalid memory, etc.
Don't think of values as fixed numbers, but rather as functions shaping a non-deterministic value. The range check made upstream becomes part of this function. The goal of the SMT solver is to find a counter-example that crashes the machine model. If you want a rather leaky abstraction, imagine that by transforming the source code into an SMT equation, it's effectively turning it inside out and transforming it into a logic equation with modulo math.
I agree with you that it would be nice if bounds were included in C arrays. But, we have to work with what we have. Unfortunately, especially in firmware that requires a commercial C compiler, we are stuck with C. In those cases, if we want safer software, we need tooling like this.
There might be some behaviors that sanitizers don't currently catch but are used for optimizations as well, and boringcc/friendly C might be useful for those? Not entirely sure what those might be, if they even exist, though; IIRC sanitizers don't currently flag strict aliasing violations, but -fno-strict-aliasing exists so there's no need for a new compiler/dialect (for GCC/Clang?)
I agree to a certain extent, but at what point are you putting in more effort than it would take to migrate to a stricter language that offers more safety by default?
I'd say that the added effort is minimal. The compiler spits out more errors. We think a little more carefully about how we write functions so CBMC doesn't complain. There are no silver bullets.
>While SMT techniques have been traditionally used to support deductive software verification, they are now finding applications in other areas of computer science such as, for instance, planning, model checking and automated test generation. Typical theories of interest in these applications include formalizations of arithmetic, arrays, bit vectors, algebraic datatypes, equality with uninterpreted functions, and various combinations of these.
I do recommend standardizing on hardware and toolchain for projects like these, as it can ensure that the machine code is matched.
The third phase of my work will round the corner with machine code analysis. Currently, I'm working on constructive proofs and equivalence proofs of imported C code to handle the pieces that CBMC doesn't do so well, as part of my second phase. So far, I can extract efficient C from Lean 4, but I want to import C directly into Lean 4 to prove it equivalent to extracted code. Hand-written C is easier to read than the line noise I can extract.
In particular, model checking doesn't fare well with data structures, algorithms, and cryptography. These can be torn down quite a bit, and at least we can verify that the algorithms don't leak memory or make bad integer assumptions. But, if we want to verify other properties, this requires constructive proofs.
Between 80 and 95 percent of the time, depending on the type of code being written, CBMC is enough. I'm now solving for that 5 - 20 percent of code that CBMC can't easily tackle.
To the contrary, that's what I'm using it for in most of my projects. It found interesting algorithmic bugs in my ctl find function (3-way comparison callback missing cases), is used to crack poor hashes. Also my tiny-regex matcher profited from it.
Also a lot of baremetal firmware.
What do you mean by standardizing on hardware and toolchain? I am currently choosing ghidra and cbmc, and it seems like the approach is applicable to any compiler or arch that ghidra supports without too much change. I have only been doing x86-64 and gcc so far though
I agree that if one isn't going to enhance C, one is going to have to resort to these tools.
C gets new features now and then. Why not add something incredibly useful, like the slice proposal? Instead, C23 got enhanced with the crazy Unicode identifiers. Richard Cattermole has been adding them to D's C support, requiring 6000 lines of code!!
https://github.com/dlang/dmd/pull/15307
The entire C parser is 6000 lines of code:
https://github.com/dlang/dmd/blob/master/compiler/src/dmd/cp...
In the meantime, check out my github. https://github.com/nanolith
Currently, I'm feeding commits into libcparse, which is the start of this effort. That stream is currently about 180 days out of phase with what's in my local repositories, but it is slowly trickling in. The first step of this second phase will be to use libcparse to verify itself via constructive proofs in Lean. Currently, and by that I mean what hits in March or April, libcparse has a mostly C18 compliant lexical scanner for the C preprocessor. The goal is to have a SAX-like C18 parser that can detect all C declarations and definitions. Tooling will include a utility that imports C declarations and definitions into both Lean 4 and Coq. This is a moonlighting effort for an upcoming book I intend to write.
OpenBSD, in particular, is adamant about testing their OS on real hardware for platforms that they support. Their definition of "working" is on real hardware. I think that's a reasonable goal.
It helps with analysis like this, as you don't have to worry about different compilers or different hardware. GCC, in particular, can produce different output depending on which hardware it was compiled on, unless a specific configuration is locked down. If there is a flaw in how it does vector operations on a particular Xeon processor, this won't be caught if testing locally on a Core i5 processor that produces different code.
I wondered if you were implementing a normalization (which is complicated), but that doesn't seem the case? I have read the PR and it's clear that it solves multiple issues at once: no start-continue distinction (bug), a code generator, the table itself (which is just an inefficiently formatted block of data). 6000 lines of code just for Unicode identifiers are simply an exaggeration.
6000 is also a round number, I did that deliberately to imply the number was not exact.
Actually, this spec security bug was added with C11. But nobody really added it, until gcc decided that this new "feature" needs to be supported around C23 time. C23 tried to mitigate that mess by disallowing non-identifiable normalization variants, but still does not care about Unicode security guidelines.
[1] https://github.com/dlang/dmd/pull/15307/files#diff-3677bcc89...