Hacker News new | past | comments | ask | show | jobs | submit login
Ask HN: What are some good resources to learn safety-critical C/C++ coding from?
42 points by bobowzki on Aug 5, 2022 | hide | past | favorite | 67 comments
I will need to learn about writing safety-critical C/C++ code at my current job. Many resources[1-2] tell you what not to do, but few tell you what to do[3].

What are some excellent examples of open source code bases from which to learn?

1: https://www.misra.org.uk/ 2: https://yurichev.com/mirrors/C/JPL_Coding_Standard_C.pdf 3: https://nasa.github.io/fprime/UsersGuide/dev/code-style.html




First, stop saying C/C++. If you are talking about C, you are not talking about C++. If you are talking about C++, you are not talking about C.

Second, give up on C. It simply has not got the resources to help you with safety. It is a wholly lost cause.

In C++, you can package semantics in libraries in ways hard to misuse accidentally. In effect, your library provides the safety that Rust reserves to its compiler. C++ offers more power to the library writer than Rust offers. Use it!


> First, stop saying C/C++. If you are talking about C, you are not talking about C++. If you are talking about C++, you are not talking about C.

I don't understand why people hate the "C/C++" term so much. C++ is (almost) a superset of C, you can mostly drop C code onto your C++ compiler and it will work.

So for OP question, C/C++ implies that he is using a C++ compiler, and an answer using C code would work perfectly fine with his C++ project (it might not be idiomatic C++, but in the world of C++ everybody does things their on way anyway).


> C++ is (almost) a superset of C, you can mostly drop C code onto your C++ compiler and it will work.

The devil is in the details. The "almost" and "mostly" parts are really important here, especially given that the entire topic is safety.

If you compile "C code" with your C++ compiler (as C++), you're no longer compiling C code; you're now compiling C++ code that looks like C.

It would be fine if C code, when compiled as C++ had one of two outcomes:

1. Behaved identically to C code compiled as C. 2. Refused to compile

Unfortunately, there are things that exist in both languages but have different semantics such as the type of character literals, e.g. 'a' (int in C, char in C++).

As for the superset thing, there's been quite significant divergence especially since C99 (e.g. variable length arrays)

Other subtly different things include:

the bool type (C++ has a bool type, C99 has _Bool with a macro for bool as bool isn't a reserved word in C).

designated initializers (C99 had them first then C++20 introduced them to C++ but with more restrictions)


None of that matters. There are plenty of other differences, and they also don't matter.

What does matter is that, while you probably can compile your C code with a C++ compiler with minimal changes, it is very bad code. It might be the best C code ever, but as C++, it is just crappy. I mean that technically: the C++ code you could have written instead would have been much, much better. As C++, that C code is just embarrassing.

This is not a reason to avoid switching your project over to building with a C++ compiler, which you could do in an hour or a day. Gcc did this, and Gdb, both with rousing success: Gcc and Gdb are now better programs, and you can tell that just using them.

Yes, after the switch, all the code will have instantly become bad code. But the secret is, it was always bad code. It was just (one hopes) the best anybody could have done in C. Now you can begin to make it into good C++ code. Incrementally, a little at a time. Probably starting with new features, and some key infrastructure, always resolving immediate frustrations. Each bit makes your program better.

Probably most of it will still look a lot like C, forever. That is the code you don't touch much, and haven't needed to. Where you get the real benefit is in places you already have reasons to change. Those are likely to be near where future changes will be, too.

There is never any confusion, looking at an evolving project, about which parts are C, which parts are in (say) 2000s-era C++, and which are modern. So, there is no value in trying to stay "consistent". New code is different because it can be. Old code is different because it still is. Some of it will change, and come to look modern, too. Most won't.

And that is OK. Keeping code consistent would mean not applying things you have learned, and not doing things better than you could have before. Inconsistent code indicates learning and growth.


The point here isn't copy pasting C code into C++ verbatim. The changes needed to translate C code into valid C++ code that does the same are very simple. One could translate many thousands of lines of C into C++ in an hour.

More so, the point here isn't even translating an existing codebase where small bugs could surface. It's just explaining concepts. None of the C to C++ differences would matter.

If an answer to OPs question was in C, he could trivially convert that C example into C++.


The things you mention are true but wholly besides the point. C/C++ normally means "C flavoured C++, or just C, I don't care", or it can mean "more modern C++, maybe do a bit of plain C or interface with C libraries". It's more about the domain you're working in and the general approach (compiled language, caring about performance, ....), and results, than about the minutiae of the language you're using.

It doesn't mean thinking of C/C++ as literally a language that you can compile, and doesn't mean compiling random C code as C++.


While I've seen predominantly C codebases that look vaguely like C++, I don't think I've ever seen a C++ (even "C with classes") codebase look like C.

In practice people don't drop C into a C++ compiler. They wrap C APIs in C++ classes and move on, or more likely inherit a C++ wrapper around C Libs (and debugging, file i/o, logging, networking, guis, etc) that look nothing like C.


Depends how pedantic you are with your "looks like C" judgement, but in my perception many hardcore programmers write "C flavoured C++, not modern or template-heavy C++" by their own account. John Carmack is only the last whom I've heard say this, just yesterday in the new Lex Fridman Podcast episode.


"Not template-heavy" mainly just means not library code. Templates, like most important C++ features, are mainly there to enable writing better, faster, more powerful, and less easily misused libraries than you could have, without.

Failing to use good libraries would be a mistake. A library is probably faster and less buggy than what you would code in place. Time spent improving libraries is amortized across all uses, so there is more of it. This is very evident in heavily used libraries.

You might not notice the templates much, but they made your experience using the library better. They have tailored the generated code to be optimal for just the way you are using it, instead of doing what would be just OK for all uses.


It can also mean not overusing libraries, not overabstracting prematurely, seeing if you can simplify the high-level design of the program to not rely on complicated compile time magic so much.

There's probably a good balance to using templates where they improve QoL but there's only very little code with those mess of < and > that tend to produce unreadable error messages. But a lot of "modern" C++ I find browsing on the internet way overuses templates IMO.

I could be considered a masochist for still writing plain C, but I find it very convenient just having one compiled (not optimized to death) version of code that is parameterized with runtime parameters (mostly sizes, sometimes alignments, rarely function pointers) in most cases. I find it also better for modularity if the library and container code is type-agnostic. It also builds faster, often much faster. Doesn't work for non-POD C++ classes, of course.

C-Pointers are often all the "generics" I need, sparing me to type out almost all of the repetitive size calculations. And even that (array access) gets rare as the projects grows more and more structure.


Often just OK is just OK. And fast builds have enduring appeal.

But even masochism can get too comfortable.


CERT C is a really good standard and book. But there's really no reason to read a book. It's very simple if you follow these steps.

Step 1. NO CONSTANT NUMBERS! All constants should be a define macro or a constant. This will allow you to change code without overflows and having to update the number in 20 places and not knowing what number to use when looping through.

Step 2. SESE(RAII in c++, but most use SESE even in c++). SINGLE ENTRY SINGLE EXIT. Your code should look like

" int *ptr = foo(); if(ptr == nullptr) DEBUG_PRINT("FAILED ALLOCATING PTR IN __FILE__ @ __LINE__) goto exit;

EXIT: if(ptr) free(ptr); .... "

So any allocations you cleanup in exit. This way you won't miss it with wierd control flows. This is reccomended by all cert c standards.

Step 3: If you can, there's analyzers you can use that will point out all bugs by annotating your code. SAL is arguable the best in the industry and you can catch pretty much all bugs.

Step 4: Even without an analyzer, you should be looking at all warnings and either adding a compiler macro to ignore it, or fixing whats causing it.


> Step 1. NO CONSTANT NUMBERS! All constants should be a define macro or a constant. This will allow you to change code without overflows and having to update the number in 20 places and not knowing what number to use when looping through.

My radix sort now needs BUCKETS_PER_LEVEL_1BYTE, BUCKETS_PER_LEVEL_2BYTE, FIRST_BYTE_SHIFT_AMOUNT, (0x100, 0x10000, ((sizeof(x)-1)*8) respectively) etc. along with many of my mmaps needing NO_FD to replace -1


Having gotos is a big no no. Put the rest of the function in an else case if need be.

https://www.perforce.com/blog/kw/NASA-rules-for-developing-s....


https://wiki.sei.cmu.edu/confluence/display/c/MEM12-C.+Consi...

Here's a copy of the CERT C standard. The site you linked is incorrect. If you google GOTO error handling. Or any of the sort it will come up with it. You should'nt use goto in literally any other case besides this though, which is what I( assume they were referencing.


Thanks, that's interesting.

AUTOSAR C++14 guidelines don't allow goto in any case.

MISRA C++ 2008 does have a couple of rules that allow this specific scenario.

MISRA C:2004, "goto shall not be used".

MISRA C:2012, it was weakened to "goto should not be used".


C and gotos are common from what I've seen -- userspace and kernel C code deals with error handling and resource freeing easier that way.

Open to see examples of (good) C code that doesn't use it though.


I used goto's fairly often to separate the happy path from the unhappy one. That's way better than confusing jumbles of if statements.

  if(something_bad)
  {
    err = BADTHING1;
    goto oops;
  }

  // more stuff

  return 0;
  
  oops:
  return err;
I'll add

1. keep code that performs calculations separate from code that performs side effects. In fact isolate the latter as much as possible.

2. Avoid spooky action at a distance. Code should relentlessly work towards a clear goal.


Linux kernel has a bunch of gotos, primarily used to just jump to exit / deallocate on kmalloc failure, without having to unnecessarily complicate the control flow to handle that.


ITT: People who don't understand safety-critical systems telling people how to write safety-critical systems.

The most popular answer in this thread is "you can only write safe C++" which is bullshit. The language that you use will likely be dictated by the toolchain you're forced to use to meet whatever standard your org has adopted. For example, if you're in the automotive realm and following something like ISO-26262, you'll only be able to use a qualified toolchain that's compatible with your safety MCU – so you'll likely be limited to C or C++, and then FURTHER limited by MISRA standards to a subset of those languages. There is no version of Rust that may be used for safety-critical systems, currently – despite the fact that it's arguably a better language, the rigorous verification/documentation work hasn't been done yet. If you're looking for an alternative to C or C++ for use in safety-critical domains, look at Ada.

You will likely not find any example of an open source codebase for safety critical systems. Rigorously-developed safety-critical systems cost millions of dollars to produce, document, run through V&V, etc. They don't tend to get released as OSS.

For the rest of the folks in this thread: type safety, memory safety, etc. are awesome features – but having a language with these features doesn't allow you to build a safety-critical system. It doesn't even begin to. If you're curious, you can start to look at the roadmap for the Ferrocene project – the company behind it is working with the folks from AdaCore (AFAICR?) to make a version of Rust for safety-critical systems a reality (one that I'm very much looking forward to!)


This book `Embracing Modern C++ safely` just showed up in my book feed, you may find it useful. [1] is a review of the book.

[1] https://www.cppstories.com/2022/embracing-modern-cpp-book/


Find the industry standards you're supposed to follow. If your job requires safety compliant code, the company should have documents that give good style guides. As mentioned by other commenters, aviation has its own standards, and you linked to some of the NASA work.

In automotive, where I've done ISO26262 work (Functional Safety standards), there are MISRA and Cert C static checkers and guidelines to make them not scream too much, not to mention the fact that you'll be following the style of the code you modify. Beyond that, you can find the industry guidelines for whatever standards you're responsible to follow. It gets worse as you get more strict -- brake controller code in the safety critical path has to meet the strictest formal methods checking as well as a bunch of in-use, on-controller testing. Generally, no one gets thrown into that without any training on the grounds of safety and liability alone.


What MISRA and Cert C static checkers do you (or your company) use?


From Stroustrup himself (consulted on guidelines for the F-35). https://www.stroustrup.com/JSF-AV-rules.pdf

Maybe stricter than you're looking for, but no memory is allocated or deallocated after the plane takes off and until it lands!



Useful resources: colleagues, professional training, case studies of errors.

If your job is safety critical software I guess they'd pay for relevant training. If not, looking at the course outlines at least lets you know what trainers think are important topics, for example

https://www.feabhas.com/content/robust-software-embedded-sys...

One training course I had talked about how to design a system with integrity while integrating open source code of unknown integrity. Since software quality and safety critical software depends so much on process, then open source by default isn't built to any integrity level. If a system needs two independent implementations of a calculation, an open source code base would never show that.

If you have an experienced safety engineer, ask them about how typically to design the system and software to make the safety case easier and they'll have some ideas of what needs to commonly be done. It depends on the integrity level what strategy and process needs to be followed.

It's not just the code style, but there's a broader mindset that you need to develop.

There's also good presentations and lectures that come up from time to time here or on YouTube where the failure of safety critical software is studied. These can be excellent case studies: Such as: https://news.ycombinator.com/item?id=31236303


Not open source, but Medtronic published a complete ventilator design and documentation, including firmware, in response to the COVID crisis.


That's very interesting!


As others have mentioned start with identifing the relevant functional safety standards for your industry. IEC 61508-3 and the annexes, whilst very verbose, is basically the textbook for safety development.

Pro tip, standards can be hard to find and expensive but you can rent or buy them cheaply from the Latvian Standards website (https://www.lvs.lv/), most are harmonised and exactly the same as IEC or ISO parent standards, just with an LVS cover sheet.

This book ,Embedded Software Development for Safety-Critical Systems by Chris Hobbs gives a great overview of safety software development in general and the key standards, I found it easy to read.

https://www.routledge.com/Embedded-Software-Development-for-...

On a practical note if using C or C++ get familiar with commonly used language subsets such as MISRA (https://www.misra.org.uk) or CERT C, again which is more relevant will depend on industry.

Gimpel's PC-Lint is a commonly used static analyser for MISRA compliance, and you can try with it on their website (https://gimpel.com/demo.html), I haven't come across a free tool complete checker but you can do a lot with clang and GCC.

Some mention of Rust here but I think that would be a hard language to get through a certification process due to the limited options for qualified tools. That said there is work being done there, https://ferrous-systems.com/ferrocene


SEI Cert C coding standard is still updated and has good advice https://wiki.sei.cmu.edu/confluence/plugins/servlet/mobile?c...


Architect your system for handling failures. No software will be bug free, because the hardware you run it is not perfect and can introduce things like bit flips. It's okay to fail, but you need to be be able to recover.


It's definitely not "okay to fail" in safety critical applications. The whole process of certification is basically checking that you've considered most of the failure conditions and designed mitigations to get the probability of safety failures below acceptable levels.


Everything can fail. Being ignorant and pretending that things don't fail is naive. You have to be ready to handle failure.

>considered most of the failure conditions and designed mitigations to get the probability of safety failures below acceptable levels.

You are just repeating what I am saying. It's impossible to get rid of failure conditions, so you add mitigations in case it happens.



Yep. This is what I was scrolling to see if someone had already posted.


dwheeler.com and adacore.com are good places to look. Even though the latter is an Ada site, you can learn things from it. Why are you stuck using C and/or C++ anyway? And what is your application? That affects the answer.

I agree with the posters who emphasize that C and C++ are not similar languages and shouldn't be lumped together, fwiw.


Start with "Safer C" by Les Hatton. It is getting on a bit now but still a worthwhile read.


I will be this guy, but can’t you use a better more safe programming language and expose an API for C?


My experience in the aerospace industry may or may not be more widely applicable, but here are my two cents.

Using "better more safe programming languages" generally gets you [some greater level of] memory safety and thread safety. Developing for safety-critical embedded systems with a single thread and no dynamic memory allocation renders those benefits irrelevant. There are still concerns around type safety, but full compiler warnings and strict code review, both of which we need regardless, handle that.

We also need to be able to certify not only that our source code matches our requirements, but that our binaries match our source code. Compiling with --c99 --debug -O0 gives us a highly visible link between each line of source code that goes into the compiler and the assembly instructions that come out of the compiler. We know exactly what the computer is actually doing, not just what we think we've told it to do. The various "better" languages all get "better" via more powerful and clever (read: complex and opaque) compilers, which is a no-go in our field.

With little benefit and impermissible cost to the alternatives, and the breadth and depth and longevity of the resources and support available for it, there's no sane choice for us but C.


This is a great reply. I'd never considered that part of this field was reverse engineering your own program to confirm the compiler actually emits what you asked it to.


There's sense in this if you don't trust compilers/ interpreters in other languages to be reliably doing the right thing, which is certainly a reason to be wary of languages that are new or aren't super widely used. But the amount of effort that goes into ensuring Go or Rust or Ada compilers always generate the correct underlying machine code is surely far more than your own team can achieve, and if there were bugs in such compilers you'd be extraordinarily unlucky to be the first devs affected by them. Also are you saying the final shipped product is built with all the debug flags on (and optimisation flags off)? If not, how do you trust those binaries- trying to match machine code output with C source when optimizations are on (and key debug info stripped) is virtually impossible much of the time.


If the debug code with no optimisations is the one trusted most, that's what ships.

If the optimiser drops off some code as it thinks it has no effect, I'd guess that's possible to spot, and you'd want to know to either fix the code or remove dead code. I've not personally had to inspect compiler output but I do spend a surprising amount of time in linker map files understanding what's going on.


Optimising compilers do a lot more than drop code with no effect (and TBH most decent linter tools will pick that up for you anyway), I've seen them generate machine code that bore virtually no relationship to the C source, with loop unrolling, function call inlining, operation interleaving etc. etc. https://cacm.acm.org/magazines/2020/2/242347-optimizations-i... has some interesting examples.


Yup, and that's exactly why we turn the optimizations off and keep them off, even for the shipped release.


Makes sense if safety is a priority over performance and/or concerns over reverse engineering.


For the mostly high-level C code that I write, I find that compiler optimisations give typically give speed-ups in the 1.5x - 3x range. A lot of code is bound by external bottlenecks that would completely mask such a speedup anyway.

For really performance-oriented code you probably want to drop to SIMD first, and play with compiler optimisations second.


I'll be that guy, then, and note that your response demonstrates that you've never worked on a nontrivial safety-critical project.

It's clear from the OP's question that the org is already C-centric. Language support in a safety environment is a large and complex issue. The compiler, tools, libraries are also required to be validated in context (depending on the standards environment), coding and other supporting standards have to be developed or vetted for adoption, you need a population of able reviewers, interoperation between implementations needs to be validated, etc. etc. etc.

And at the end of the day, the "safety" that another language buys you doesn't actually get you very far. A lot of folks get hung up about memory safety, or this or that language feature, when in reality the majority of safety issues in large codebases are algorithmic in nature, and no low-level language feature is going to save you from implementing the wrong design.


Yep, this is why in Vulnerability Research one of the main ways of finding bugs is concolic execution (Looking at logic for error cases). Memory safety is pretty easy to catch if you arn't a clown and use good coding patterns. Logic isn't, which is where the big money and big boy vulns come in. And honestly, most modern OS's have memory safety protections built in. This is 1992 anymore.


Yes, memory safety is a solved problem, and research has mostly moved on. The best solution is to use a memory-safe programming language. Yet we find security vulnerabilities all the time in software written with memory unsafe programming languages.


Sure, but safety-critical development is a different domain than vulnerability research.


"Memory safety is pretty easy to catch" - my experience over the last 18 months or so on a large C++ open source project says very much otherwise, almost every week a new NPE or core dump/access violation seems to leak into the codebase. These simply can't happen in many other languages and many now even have good support for non-optional references, ruling out entire classes of bugs that can suck up significant developer time to track down. And for long running systems memory leaks can also be a substantial cause of instability. If I were working on a safety critical system I'd actively push to avoid using C/C++ unless there really was no alternative. And yes modern OS kernels are pretty good evidence that it is possible to do but I'd suggest it requires exceptional programmers and some very rigorous processes to enforce.


Assume you have a functional safety requirement for which a standard exists. What language implementation would you use that is certified for that standard?


Not sure how that question can be meaningfully answered unless it's for a specific safety requirement/standard. Are you suggesting that for most standards that require certification, there are no compiler implementations certified except for C/C++ ones?


Try the exercise. Choose one and then choose your tooling. If it's not C, C++, or Ada, I'll be surprised.


I had some involvement with development of an air traffic management system, which was being written in Java, as that was certified for use in that field. As a language, I'd rather use C++ over Java, but if safety criticality were the priority, I'd choose Java every time. I haven't coded in Ada but unless there's something about it that's truly awful to work with, it seems a better choice than C/C++ from what I know about it.


Well, I have worked on safety-critical projects in the industry, though there wasn't a lot of C/C++ (health-care software connected to the internet). I currently work with researchers, some in the software security domain.

I don't think it's clear that the OP organisation is stuck with C. Perhaps it's the case, but I think it's also time, in 2022, to push a bit to move toward safer programming languages.

You describe an excellent set of rules and processes, perhaps too much for most projects. It sounds like it could be even better with a memory-safe programming language. Also, I doubt that most secure critical C/C++ projects proof their source code correctly in Coq or similar.

I also think many security issues are tightly related to the programming language. For example, SQL injections are because of SQL, XSS because of HTTP/HTML, and Buffer overflows because of C.


Security and safety are different domains and only tangentially related - i.e. it helps in both domains for code to be correct, but it's not sufficient in either.


One take away I got from reading some Nancy Leveson stuff is a lot of danger lurks at the interfaces between things. You can take two things that work perfectly and put them together and get catastrophic behavior. That argues to the point that there isn't a magic language that will make problems go away.

Another take away from an old old paper was that safety must be a primary design goal from the start. It's not something you can bandaid in afterwards. There is a lot of stuff out that what that didn't happen and as a result you see constant whack a mole with critical bugs.


Seconded: Is insane that somebody "make safe program" but not USE a safe program for it.

You can justify it by difficult of bootstrapping or a lot of legacy code that is already vetted so you can reuse. But in 2022, your options are very good in this area.


UGH! Why there’s always a Rust cult follower ruining every C thread with their “opinions”?


I didn't mention Rust or any language because depending on the problem, many other programming languages may be better too, such as Golang, C#, Python, Java…

I know that some people get upset when they have a question, and they don't get the answer but some suggestions. But sometimes, people are not aware of the alternatives, and it's important to remind them that perhaps a better solution exists.

If someone asks how to cut crusty bread with a butter knife safely, you can talk about the proper technic, how most people do it wrong, write a book about your industry do it better, but perhaps, someone should also mention that it's easier and better with a bread knife.


There's many other issues to mention, but in the "memory safe" languages you have poor control over memory allocation and fragmentation, which over time can cause issues with performance, size, swapping, etc.


What happens when you're using a really nice sharp serrated bread knife and a terrible cutting process?


How to program C++ - Deitel & Deitel


Write MISRA compliant and you good!!


MISRA doesn't say anything about soft errors. You can write perfect MISRA code and the first ray garbles your logic. It also doesn't say anything about common design principles like "black channels". It also doesn't say anything about what a safe state is, when to go into one, and how to make sure you're reaching it (even when your interrupt controller is misbehaving). It also doesn't tell you anything about how to safely recover from such a safe state and go back into normal operations. It also doesn't say anything about when you're needing two or more controllers, and it doesn't say anything about making sure that both controllers are executing the same code on the same data.

Unfortunately, safety code practices are highly dependent on your field and its practices, and I'm not aware of a good book or course. You mostly learn it by osmosis when joining a team that develops safety-related systems.


>Unfortunately, safety code practices are highly dependent on your field and its practices, and I'm not aware of a good book or course. You mostly learn it by osmosis when joining a team that develops safety-related systems.

You should have lead with this. Every "use a safe language" or "follow these guidelines" post that comes up when the subject of safety-critical software comes up needs basically this response.

Also, you will almost never be designing a safety-critical system in a green-field domain where no one has ever done anything like that before. So, there will be standards. You can learn a lot by reading and following the standards.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: