Vale 0.2 is out, and it includes the beginnings of a feature we like to call Fearless FFI.
This is part of Vale's goal to be the safest native language. 0 Most languages compromise memory safety in some way which can lead to difficult bugs and security vulnerabilities.
Vale takes a big step forward here, by isolating unsafe and untrusted code and keeping it from undermining the safe code around it.
This page describes the proof-of-concept we have so far, plus the next steps. It involves some borderline-insane acrobatics with inline assembly, bitwise xor and rotate, and two simultaneous stacks. Buckle up!
If you're impressed with our track record so far and believe in the direction we're heading, please consider sponsoring us on GitHub! We can't do this without you, and we appreciate all the support you've shown.
Most languages have a Foreign Function Interface (FFI) to enable calling into another language's code.
Normally, when a safe language's code calls functions written in an unsafe language, any bugs in the unsafe language can cause problems in the safe language. For example:
This is called "leaky safety", and its bugs are very difficult to track down, because their symptoms manifest so far from their cause.
This can also happen when a language has unsafe escape hatches. If some unsafe code corrupts some memory, it can cause undefined behavior in safe code. For example, see this Rust snippet where an unsafe block corrupts some memory that's later used by the safe code.
In all these cases, we know that the unsafe language was involved somewhere in the chain of events, but since the bugs actually happen later on, in supposedly safe code, there's no easy way to identify which unsafe code was the original culprit.
Worse, some people take advantage of these intentionally, by introducing these vulnerabilities into your dependencies. For example, the GitHub Advisory Database describes thousands of vulnerabilities in dependencies, even ones written in normally safe languages like Go and Rust.
By native, we mean not running in a VM.
We have two goals here:
This doesn't just apply to C code, but any code written in an unsafe language.
Vale protects against these bugs with a handful of different mechanisms, which combine to form Fearless FFI:
Let's explore each of these mechanisms!
In Vale, we often designate objects as immutable. When we send immutable data between C and Vale, we're actually sending a copy.
The C code can do whatever it likes with this copy, and there's no risk of corrupting Vale objects.
Here, a Vale main function is sending an immutable Vec3 struct into C.
We use the exported keyword to make it visible to the C code, and automatically generate the headers (like mvtest/Vec3.h).
Copying data back and forth is great for most use cases, but we also want to be able to hand C some pointers to our Vale data. That way, C code can call methods on our Vale objects.
For example, we might want to wrap a C HTTP server and have it call into Vale to handle requests. 2
However, in most languages, it's risky to make a pointer to an object and hand it to unsafe code. The unsafe code could corrupt the Vale object, like in this example:
Here, the C function myproject_halveFuel accidentally overwrites a pointer with an integer.
Luckily, Vale prevents this. Vale doesn't give C a pointer that it can use to dereference our Vale objects.
It instead gives a wide generational reference, which contains:
For those unfamiliar, a generational reference is a reference containing a pointer to an object, and a "remembered generation" which is an integer that matched the "actual generation" integer from the object itself. When we free the object, the object's actual generation is incremented. Before dereferencing a generational reference, Vale asserts that the generations matched, to ensure we haven't deallocated the object since then. By our last measurements, generational references are over twice as fast as reference counting, and could get even faster when we add our planned region borrow checker and hybrid-generational memory features.
The "wide" generational reference is then compressed into 32 bytes 3 and then scrambled:
How does C read the data then?
The C code will need to hand it back to a Vale function, like the example here.
When a Vale function receives a scrambled reference, it will unscramble it and generation-check the region. If the C code gave an invalid reference, it will be detected right then.
There are quite a few unused bits in pointers, that can be repurposed for other things.
Generations are different every run (and so are addresses, thanks to Address Space Layout Randomization), so the entire wide generational reference is random. This randomness is useful further below, when giving these references to sandboxed subprocesses.
This "pointer obfuscation" isn't for security, it's just to prevent accidental memory corruption from C. We'll talk about additional measures for security below.
One of the reasons it's risky to call into an unsafe language is because they can do buffer overruns on the stack, like this C snippet:
This function is particularly sinister, because it will overwrite its caller's memory. What if our caller was this Vale function?
Here, the C function is reaching backwards in the stack, into the caller's memory, and changing something there. This might make ship.engine point to address 0x7, because ship lives on the stack.
To solve this particular problem, the Vale compiler runs the C code on a secondary stack. Basically, it sets the stack pointer register to a new chunk of memory, to serve as our new stack.
This involves some inline assembly, which will set the stack pointer to some new memory, and then call a "wrapper" function using that new stack.
As you can see, Vale calls some sort of badCFunction_wrapper on the "new stack".
This automatically generated wrapper function will call badCFunction, and when it's done, it will jump back to our original stack.
Here is the wrapper:
Notice the longjmp, which is another way to switch stacks. Here we're using it to switch back to our original stack. Our "original stack state" was stored (and scrambled) in thread local storage.
Remember that assembly code we saw above? Below we see it in context. This C code sets up the original stack state, puts a pointer to it in thread local storage, and then uses the assembly code to switch to the new stack.
We also make sure that Vale will never reuse memory previously freed by the C code. This can happen if C calls free and then Vale calls malloc. Instead, we plan to use a separate address range for all Vale allocations, using mimalloc (note that this is not implemented yet, only planned).
So far, these mechanisms protect us from accidental corruption. With the system above, it's pretty much impossible to accidentally corrupt Vale objects. With this, projects or teams can call into their own C code, and have confidence that they aren't causing memory unsafety in their Vale objects.
This system also protects us from accidental memory safety in our dependencies. Dependencies' unsafety is a big problem in projects nowadays.
With these mechanisms, we can be confident that any problems in our dependencies won't cause memory unsafety in our code.
We implemented a proof-of-concept of this! The proof of concept works for macOS by simply supply --enable_side_calling true to the valec invocation. The other measures (copying data, references, scrambling) already work on all platforms and are enabled by default.
There are two more problems to solve here:
We'll need something additional to protect against these problems.
We believe these problems need to be solved at the language level (or below), so our plan is to introduce sandboxing for untrusted third-party C code. Basically:
A Vale program might use different strategies per dependency:
As we implement the sandboxing portion, we'll be posting follow-up articles about the details and tradeoffs involved here. Stay tuned!
Of course, memory unsafety isn't the only source of vulnerabilities. For example, a dependency could read and write files maliciously.
For this reason, we plan to have whitelisting. Specifically, every project must explicitly allow every dependency on a library which uses FFI.
For example, if we have:
Then MyProgram's valec will need to explicitly specify that ListDirectoryLib is allowed to depend on AFileLib which does FFI, with the flag --allow_ffi ListDirectoryLib:AFileLib.
This will also be required for standard library modules, such as stdlib.Subprocess, stdlib.File, stdlib.Network, etc. Any dependency using any of these will need to be explicitly whitelisted by the ultimate program.
This will, in effect, give Vale compile-time per-module capability-based security, which should help mitigate problems from dependencies.
Of course, that only applies to Vale code. For native code, we can sandbox the subprocesses, or use WebAssembly System Interface (WASI), which gives us capability-based security for the dependencies that we run in webassembly.
This could be a great boon to the software world. Capability-based security could help mitigate a lot of supply-chain attacks, like the ones explained in Supply chain attacks on open source software grew 650% in 2021 and Backdooring Rust Crates for Fun and Profit.
We've described five mechanisms to help protect our Vale data from problems in our C code:
This should give Vale programs the tools to run their native code and dependencies with much more confidence.
Thanks for visiting, and we hope you enjoyed this article!
In the coming weeks, we'll be writing more about our "deterministic replayability" proof-of-concept which eliminates heisenbugs and helps us reproduce race conditions, so subscribe to our RSS feed, twitter, or the r/Vale subreddit, and come hang out in the Vale discord!
If you found this interesting, please consider sponsoring us:
With your help, we can bring more of these features into the world!
- Evan Ovadia
The Vale Language Project is not just about making Vale, it's also about exploring, discovering, and publishing new programming language mechanisms that enable speed, safety, and ease of use.
The world needs more exploration here! Currently, most programming language research is in:
These are useful, but there is a vast field of possibilities in between, waiting to be explored!
Our aim is to explore that space, discover what it has to offer, and make speed and safety easier than ever before.
In this quest, we've discovered a lot of new techniques:
These techniques have also opened up some new emergent possibilities:
We also gain a lot of inspiration from other languages, and are finding new ways to combine their techniques:
...plus a lot more interesting ideas to explore!
The Vale programming language is only one combination of the features we've found. Our goal is to publish all the techniques we've found, even the ones that couldn't fit in Vale, so that other languages can make strides in this area.
Our medium-term goals:
We aim to publish articles biweekly on all of these topics, and inspire the next generation of fast, safe, and easy programming languages.
If you want to support our work, please consider sponsoring us on GitHub!
With enough sponsorship, we can: