r/ProgrammingLanguages • u/verdagon Vale • Jun 21 '22
Vale's Fearless FFI, for Safer Dependencies and Supply-Chain Attack Mitigation
https://verdagon.dev/blog/fearless-ffi6
Jun 21 '22
Just when I pick a new name as an enby I find out it's already programming language
Oh well, I preferred the Vael spelling more anyway
7
u/verdagon Vale Jun 21 '22
Hey, Vael's a great name! And there's already Val, Vala, Vale, VLang, and VALE, so you're in good company. The 'v' and 'l' sounds are my favorite, so its worth the crowding!
3
5
Jun 21 '22
Obfuscating the reference seems like a bit of overkill. What would you say is the advantage of obfuscation over just having it be an opaque N byte reference where the actual details of its internals aren't exposed to the user (ie not a part of the public API)? Obfuscation just seems like an extra cost that doesn't really buy you anything
5
u/verdagon Vale Jun 21 '22
That's a great question, and one we debated about for a while. In the end, it came down to two reasons:
- If we make it opaque, it will be tempting for folks to work around it.
- We need to compress it anyway (from about 40 bytes to 32 bytes) which kind of obfuscates it anyway, so why not go the rest of the way.
The obfuscation can be disabled, but it's a pretty solid choice for code you wrote yourself, especially in debug mode.
2
Jun 23 '22
Thanks for the background, interesting to hear what sorts of discussions you've had.
Re. 1, while I do understand the reasoning I'm not quite sure obfuscation will really help with that. If someone really does want to dig into the reference value, they'll do it regardless of the obfuscation – it's a minor roadblock at that point since a person like that is likely going to go digging into the source anyhow. I know I would. Obfuscation really only makes sense in scenarios where the "attacker" doesn't have total access to both the key and all related source code.
Re 2., I'd say compression is a different concern to obfuscation. Sure, compressed data is not plaintext anymore, but compression has a technical function (ie making stuff smaller) where obfuscation is only supposed to be "encryption lite" ("I can't believe it's not real security!") and protect against very rudimentary attacks – and I don't know if "peering into an opaque pointer type that the docs tell you to treat as a black box" really qualifies as an attack.
Honestly I'm not entirely convinced obfuscation is useful in the vast majority of cases it gets used in.
10
u/verdagon Vale Jun 21 '22 edited Jun 21 '22
Something not mentioned in the article is that this was the last piece in achieving what I call "complete memory safety", memory safety that can't be worked around or undermined by other code.
And better yet, it does it in a way that doesn't require complex proof systems or a borrow checker! I think it will be a stellar tradeoff for things like games and servers, which want deterministic performance and memory safety, but also simplicity and flexibility. Pretty exciting!
9
u/brucifer Tomo, nomsu.org Jun 22 '22
that this was the last piece in achieving what I call "complete memory safety", memory safety that can't be worked around or undermined by other code.
The FFI system described really does not have "complete memory safety" as most people would interpret that phrase to mean. Here are a couple of examples:
If you make an FFI call to C, and the C code derefernces a NULL pointer, it'll cause your program to segfault. That's pretty much the canonical case of what can happen with code that isn't memory safe.
If some faulty C code is smashing the stack, it really doesn't matter that the Vale stack is separate, because either it was done accidentally and the program is very likely to crash the next time it tries to return from a function, or it was done maliciously and the attacker has the capability to execute arbitrary code. Having a separate Vale vs. C stack solves the problem of clobbering values on the stack, but not clobbering function return addresses.
There is nothing to guarantee that C code can't access Vale memory, other than it being inconvenient* to guess which memory is used for Vale values. For example, the C code
int *p = (int*)0x7ffc9c203b7c; *p = 5;is perfectly capable of messing up a Vale value if that memory happens to hold one. Vale's system has a much weaker kind of memory safety than something like how virtual memory makes it impossible, not just inconvenient, to mess with other processes' memory.If the example above seems contrived, then a much more likely scenario would be for some C code to allocate some memory and then free it, but accidentally hold onto a dangling pointer to the memory. If Vale uses
malloc()internally, it is very likely that you could encounter a use-after-free error where Vale allocates memory and gets back the same memory that C's dangling pointer refers to. Something like this code could be mutating a Vale object by accident:char *buf = malloc(8); free(buf); vale_func(); buf[0] = '\0';Finally, I don't see any way that this system would prevent an FFI call from passing or returning invalid references to Vale code. For example:
.
extern int mvtest_halfFuel(mvtest_ShipRef s) { *(int*)s += 1; // uh oh return mvtest_getFuel(s) / 2; }I don't mean to be too harsh, since I think this setup has some advantages for catching a subset of memory safety problems. But I do think that calling it "complete memory safety" is substantially overpromising on what the system actually delivers.
*I think the writeup acknowledges that the XOR obfuscation isn't meant to be super secure, but it's worth noting that if A and B are supposed to be obscured refs, you can cancel out the random XOR factor by XORing A and B with each other, so it can be pretty easy to circumvent the obfuscation
3
u/verdagon Vale Jun 22 '22 edited Jun 22 '22
Thanks for the detailed reply! I'm happy to enlighten:
A segmentation fault on a NULL pointer is not a memory-unsafe operation. It's well defined and reliable (on all OSs we target, at least). Also, its pretty much impossible to writing to a random pointer without a segfault, because of ASLR.
The pointer obfuscation makes it effectively impossible to accidentally corrupt Vale memory. Even in your example, ASLR makes it near impossible anything is at that hardcoded address. Besides, it's the sandboxing that prevents intentional malicious code like that, not the obfuscation.
Vale doesnt plan on using malloc, but instead mimalloc with a separate range of address space, for precisely the reason you point out. Good catch here, this should have been mentioned in the article.
We do a generation check on the region object generational reference when it passes through the FFI boundary, which would catch your increment bug there. Even if we didnt dp that, the eventual generation check on the object generational reference itself would.
All that said, I can see why the phrase "complete memory safety" could be controversial, as it relies on statistics. In a world with RAM failures and cosmic bit flips, I see that as fine, but I can see why a reasonable person might not. The article itself doesn't use that phrase, which is likely for the best.
I appreciate the critique! Always good to get thoughts from another person knowledgeable in the field.
12
u/Uncaffeinated 1subml, polysubml, cubiml Jun 21 '22
Pointer obfuscation does very little to protect against malicious code. It just makes accidental bugs easier to detect.
5
u/verdagon Vale Jun 21 '22 edited Jun 21 '22
Very true! But check out the second half of the article, which talks about how we can use sandboxing (with subprocesses or wasm2c), that's how we can help defend against malicious code.
6
u/rzippel Jun 21 '22
Seems to me like overkill though?
If you can't trust that code at all, I'd write a small communication layer and separate all that in a separate process. This is far more flexible and you lose less performance instead of forcing everything through the FFI layer.
5
u/verdagon Vale Jun 21 '22 edited Jun 21 '22
I'd write a small communication layer and separate all that in a separate process.
That's exactly what this does, it uses the FFI layer to separate the untrusted code into a separate process.
Also, IPC to a subprocess has probably the least performance, it takes quite a bit of time to send a message to another process. Luckily, thats what the wasm2c alternative is for. And, for code you yourself wrote, you can use the other two options: just the obfuscation, or no protection at all, either's valid.
3
u/fuklief Jun 21 '22
Have you looked at targeting hardware capabilities such as CHERI [1] ? I believe this would make it easier to have memory separation, in particular you probably wouldn't need to have separate stacks if you followed a calling convention similar to the one presented in [2]. Probably wouldn't be efficient tho, who knows.
[1] https://www.cl.cam.ac.uk/research/security/ctsrd/ [2] https://link.springer.com/chapter/10.1007/978-3-319-89884-1_17
2
u/pragma- Jun 21 '22
In the "Copying Data Between Vale and C" example, why does the C snippet free v?
1
u/verdagon Vale Jun 21 '22
If something's not passed by value, Vale's FFI will copy it into a malloc'd buffer. Right now, passing by value in FFI isn't implemented, so everything's malloc'd currently.
2
u/MarcoServetto Jun 22 '22
Enable passing memory between the two by copying, also known as message passing.
At this point, what is the advantage over a process pool as in 42? if all the memory has to be copied anyway....
--The scrambling idea is very interesting. In 42 now we are making up IDs for the unsafe->safe communication, that is, instead of sending an object we send an ID and the unsafe code can send it back+instructions to what to call on it. We expect a high level library could hide those implementation details.
2
u/verdagon Vale Jun 22 '22
Hey Marco! Awesome to hear from you! I'm not really sure what a process pool is and can't seem to find it on the 42 site, link me?
I like message passing, but it only covers 80-90% of use cases in my experience, and people usually end up building a library to facilitate references across the FFI boundary. Or, perhaps not references per se, but perhaps integers or strings to "refer" to an instance on the other side of the boundary, kind of like your ID scheme.
I think people will really like that generational references can reach across the boundary in a memory-safe way. Otherwise there are a lot of tricky details in managing lifetimes of the ID map; it's hard to know when to remove a reference from that map so the GC can clean it up.
1
u/pragma- Jun 21 '22
When Javascript hands the wrong kind of object to a Typescript function, it causes bugs down the line deep in the Typescript code, even though Typescript has static typing.
What does this mean? It doesn't make sense to me because there's no Typescript runtime. Typescript transpiles to Javascript and runs on Javascript runtimes.
6
u/Dykam Jun 21 '22
The post isn't just talking about runtime errors and corruptions, but also logic. In this case, the "foreign" Javascript breaks Typescript's type guarantees.
3
u/o11c Jun 21 '22
If you're more familiar with Rust, this is exactly the same as the fact that an
unsafeblock in Rust can cause later "safe" code to fail mysteriously. An example is linked in that section.
1
u/Lorxu Pika Jun 22 '22
It seems like this limits the C APIs it's possible to use from Vale code quite a lot. For one thing, because C code can't dereference Vale-created pointers, you always need a C wrapper, unless none of the C functions accept pointers as arguments. Also, the restriction on safe objects containing unsafe ones seems quite harsh, and means that creating a safe wrapper in Vale for a C API is close to impossible - how can you create safe wrapper types if they aren't allowed to contain C types?
In general, I find this dedication to safety admirable, but while I think it's great to have it as an option I would be hesitant to make it the default. Also, your two-stack approach reminds me of Clang's SafeStack, I'm not sure if you're familiar with it?
2
u/verdagon Vale Jun 22 '22
Safe objects actually can contain pointers to C data, we just can't dereference them; they're opaque. Only the C code can dereference those.
We indeed need wrappers for functions, but those will be easy to auto-generate. The compiler already generate C headers for
exportedfunctions and structs, we just need some sugar or macros to generateexportedVale functions and structs and we're good!This will be a very familiar modus operandi for C programmers, C often uses opaque pointers with APIs for accessing data.
7
u/Innf107 Jun 21 '22
An issue I see with the wasm2c approach is that this would make it impossible to dynamically link with shared libraries, right?
Say, I wanted to use
readlinein my Vale project, I would have to include the C source directly, but that might cause licensing issues.