Skip to content

Correct-by-construction Nim programs #222

Open
@mratsim

Description

@mratsim

This RFC outlines a potential roadmap to have tooling that significantly address a "new" class of bugs, logic bugs, i.e. bugs in the program design.

New as in the only general purpose language that addresses it as far as I know is Ada/Sparks. And others are formal verification language. I am aware of Eiffel and the design-by-contract paradigm, you will find assume, invariant, precondition, postcondition and assert as useful.

This is a followup to my comment on the safety modes RFC: #180 (comment)

Motivating examples

You are designing software for one of the following scenarios:

  • A safety system that prevent opening doors while a vehicle is moving
  • A scheduler for complex urban crossroads and you want to prove that the green lights cannot lead to vehicules crossing lanes of each other.
  • A scheduler for train tunnels with only one lane in the tunnel and you don't want train collision
  • A microwave and you want to ensure that under no circumstances radiations are emitted before the door is closed
  • An alarm system for a building or as a medical device and you want to ensure that there is no false negative.
  • An aircraft autopilot and you want to ensure that at any time a human can take back control to address unforeseen events
  • A critical section algorithm but you don't have any locks available on your platform
  • A multithreading runtime and you want to ensure that it is free of deadlock or livelock
  • A threadsafe hash-table and you want to handle properly many threads.
  • A distributed database or filesystem protocol and you want to prove that any sequence of prepare-commit-rollback leaves your database in a consistent state
  • A P2P messaging protocol and you want to prove that starting from a certain connectivity threshold, messages are guaranteed to arrive (and maybe their order?)
  • A blockchain protocol and you want to prove the percentage of malicious actors it can accomodate before the consensus algorithm breaks.

Overview

The usual software bug detection tools:

  • avoiding nil pointers
  • checking bounds accesses
  • sanitizing memory accesses
  • borrow checking
    only addresses low-level software bug.

They do not address bugs in software design that come from an unlikely sequence of events that trigger an unhandled or undefined scenario and potentially catastrophic failures.

Caveat emptor

This is not a replacement for runtime checks, testing, fuzzing. It ensures that your design (aka blueprint) is correct but your implementation might be buggy (for example you forgot to initialize a variable before use).

Correct-by-construction mission-critical system

In Nim, this would involve a branch of formal verification called model checking.
This works by exhaustively checking all the possible state of your program (say no train in tunnel, train approaching, trains approaching, train out of the tunnel, rain, train stuck in tunnel, red light failure, ...) and asserting that the unwanted scenarios cannot happen according to the state transitions of your system and your explicit assumptions (the red light cannot fail).

This requires at minimum:

  • A Domain-Specific Language to express state, the (potentially conditional) transitions to other states, the events that trigger those transitions
  • A way to express constraints: asserts at least, preCondition, PostCondition, Invariants
  • A model checker (prover) that can explore all those states and prove that whatever the sequence of events those constraints are respected

Paper

This paper, which comes with an implementation, uses:

  • the B formal language to express state machine, events, transition to new state, conditions and invariants (we don't need all those maths don't worry)
  • ANTLR to generate a parser the language grammar
  • Z3 with Temporal Logic backend for proving
  • Some glue to transform the state machine into Z3 inputs.

This was created by students in a trimester so should be quite accessible.

Note that they mentioned that Z3 wasn't ideal due to not mapping perfectly to the B language. In our case, I think it's an excellent first step as:

  • We don't use the B language, we can start with a restricted subset of use cases that map well with Z3.
  • We don't have to implement a prover
  • State to handle grows exponentially

In Nim

  • The Z3 backend and accompanying assertions/preCondition/postCondition checks are WIP in DrNim
  • I wrote a state machine generator that works at compile time, Synthesis, the code generated is suitable for embedded devices: no allocation at all, just if and goto and it's fast.
    • No math, a state machine is declarative
      behavior(awaitFSA):
        ini: AW_CheckTask
        event: AWE_HasChildTask
        transition:
          profile(run_task):
            execute(task)
          profile(enq_deq_task):
            localCtx.taskCache.add(task)
        fin: AW_CheckTask
    • The state machine is code first and not diagram first.
    • It can generate graphs as well (see the implementation of await that participare in work-stealing while waiting) image
    • It's a very small library (<1000 lines)
  • TODO: Translating transitions into Z3 AST (covered in paragraph 4 of the paper via AST rewrite)

Correct-by-construction thread synchronization

This is something I critically need in Weave / Project Picasso (#160).
I did use model checking via TLA+ to debug and remove deadlocks in my backoff algorithm (that put idle threads to sleep) and ultimately uncover a wakeup bug in Glibc condition variables (mratsim/weave#56).
However, that requires learning a new language (which is worth it if implementing a lot of distributed protocols) and does not address implementation bugs due to misunderstanding the C11/C++11 memory model for atomics synchronization.

Examples

Verifying the implementation of a concurrent channel.

In Nim

The goal is to verify that the implemented Nim code does not lead to an inconsistent state by unanticipated interleaving of thread execution.
The way to do that is by overloading createThread, and synchronization primitives like Atomic, Lock and CondVar.

To model check a concurrent data structure instead of creating a thread, createThread will create two states, and then you create a graph of all possible threads (and resulting states) interleaving. When encountering a synchronization primtives, the model checker might have 1 state make progress (or not) if the memory model allows it (locked or not, acquire/release, ...).

This create a (potentially huge graph) of thread interleaving and what happens at each synchronization and exhaustively explore all possible state proving whether or not there is a synchronization bug (via asserts in the code).

References:

And a lot more at mratsim/weave#18 but most only model sequentially consistent execution (i.e. synchronization via locks and condition variables) but not C11/C++11 relaxed memory model and synchroniation via acquire/release atomics).

Note: it's probably possible to use Z3 here as well.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions