Smoltcp: A small TCP/IP stack in Rust
github.com
github.com
This is explained in more detail in the Rust documentation: https://doc.rust-lang.org/nomicon/working-with-unsafe.html
"[...] Because it relies on invariants of a struct field, this unsafe code does more than pollute a whole function: it pollutes a whole module. Generally, the only bullet-proof way to limit the scope of unsafe code is at the module boundary with privacy."
That is, it's not enough to read just the block of code marked "unsafe". You also have to consider which invariants that block of code depends on, and all the code which could affect that invariant. But it's often easy to constrain what can affect the invariant; in the example given in that page, the invariant is that the "capacity" field in the struct must exactly match the amount of memory allocated for block of memory stored in the "pointer" field (and the "length" field must not be greater than the "capacity" field). Since neither field is "pub", only code in the same module can modify either of them without using "unsafe" itself (with "unsafe", one could in theory use "transmute" to access even private fields).
Though, keep in mind, this isn’t something especially costly about Rust. To properly analyze any low-level language without unsafe{} requires treating every LoC to this extended audit.
For this particular library, most unsafe blocks are for calling glibc functions via FFI. They could probably use nix to avoid unsafe in FFI with libc.
Though nix also just wraps the FFI unsafe blocks. I'm also very "meh" on nix -- it seemed like a neat project when I first started looking into using it but the interfaces they provide make it harder and less idiomatic to hook into libc.
For instance, all of the *Set interfaces are significantly more annoying to use than just bitwise or in C.
So? How they provide safe APIs doesn't matter, what matters is that they are safe. If the C FFI API is always safe for all inputs, then wrapping it in an unsafe block is the right thing to do.
> but the interfaces they provide make it harder and less idiomatic to hook into libc.
The interfaces they provide are both safe and portable over pretty much all UNIX-like systems (MacOSX, Linux, FreeBSD, OpenBSD, NetBSD, DragonflyBSD, Solaris, ...). libc interfaces are platform specific.
But my issue with nix wasn't portability, it's that quite a few of the APIs they provide are more-frustrating-than-necessary.
You could establish the same convention for C++. Let's say that any C++ module that exposes an unsafe API is at fault for doing so. Great, now we can localize the "blame" for any given segfault to the module containing the code that actually dereferences the invalid pointer. Of course, the bug is just as easy or hard to fix as it would have been without this semantic convention. Maybe it's an easy one line fix. Maybe modifying the module to have a safe API would require a total rewrite of the rest of the codebase.
You can’t make a convention in C++ in the same way, because the safety aspect is not part of the language. I mean, you can, but it won’t help you the same way Rust will.
This is wrong AFAIK: having two mutable references to the same thing is undefined behavior even within an unsafe block. An unsafe block only allows extra functionality (like dereferencing raw pointers), it doesn't change the semantics of functionality that's already allowed outside an unsafe block. That is: what would be undefined behavior outside an unsafe block is still undefined behavior inside an unsafe block.
Has "safety" become a buzzword? What we really strive for is correctness. Hypothetically, Rust could still be detrimental to software correctness compared to a more usual language, if some of its traits as a language (eg., complexity) encourage bugs. Sometimes it seems like Rust afficionados think that buffer overflows etc. are the main kind of bugs.
And if you need absolute correctness (when human life depends on the software) you would probably use Ada and Spark or something else that enables you to actually prove correctness.
Correctness is about program making what I meant as a programmer. Safety is a different thing. I see it as insulator on wires. It helps me to avoid shocks while working with electricity. Safety guarantees of Rust is like that, I have a pointer, I could use that pointer without fear of segmentation fault or data race. It feels like safety. It named "safety". So I believe it is a good coherent name.
> if you need absolute correctness (when human life depends on the software) you would probably use Ada and Spark or something else that enables you to actually prove correctness.
Maybe. But between absolute buggy code and absolute correct code there is a spectrum of not so buggy and not so correct code. We need a compromise enabling us to create a reasonable reliable software with a reasonable effort.
Have you looked at the CVE stats of the last decade recently? Memory errors make up around 3/4 of that. Even if Rust would make the last quartile harder (which in my experience it doesn't), it could still be worth it for many applications where you cannot afford full verification, but want to avoid your users being pwned.
Keeping planes from crashing and bank accounts correct matter too. Rust solves a subset of memory safety problems but it is not a programming language for high assurance applications and in that context enables many other types of unsafe behavior.
Most systems programming languages don't make it simple and many people don't do it but it is definitely valuable and worth doing when the economics make sense.
Even with a fully verified toolchain there will still be bugs. I once had a customer find a rare bug in a database engine that was ultimately caused by slight differences in behavior between microarchitectures running the same binary.
Tautologically so.
"Which of the following tools do you or your team use for guideline enforcement or other code quality/analysis?"
https://www.cvedetails.com/top-50-vendors.php
Microsoft 6508/116769 = 5.5%
Apple 4502/116769 = 3.8%
Google 4110/116769 = 3.5%
total (6508 + 4502 + 4110)/116769 = 13%
The complexity you’re probably referring to is the type system and/or borrow checker. The type system is strong enough that, if you take the time, correctness can be encoded into the types. Meaning, you can build types that make it a compilation error if the software is not “correct”. At that point even refactors can be more assured to meet those encoded correctness guarantees. So I’d say that the type system helps develop software that is provably correct.
Then there’s the borrow checker, and that’s about runtime memory access correctness, again increasing correctness.
Point being, Rust programs because of the “complexity” can get much closer to correct than other languages in its space.
In my experience, lack of memory safety is one of the largest sources of non-trivial bugs I experience. Sure, if you are writing truly mission critical software for planes or pacemakers, you are going to need stronger (and different) guarantees. But Rust's goal is not provable correctness in the Coq sense. It's to provide a productive interface for designing efficient and memory safe/thread safe programs.
Computers are all around us, and they are a security nightmare, less because of their lack of correctness than because of their lack of safety. That's why Rust is important.
Formal proofs and tools like Ada are important too in their own domain, and Rust isn't competing with them (yet at least, I know there are some people would like to develope something akin to Spark for Rust adding best in class correctness tools to Rust's toolkit).
An example of something that's incorrect but not unsafe would be, say, an error which would occasionally corrupt random TCP packets causing checksums to fail and the packets to be retransmitted. It's not working how it should, but it's not compromising your system's security or your data's safety (at least I don't think it is).
There used to be a lot a vulnerabilities in the Java plugin of browsers, but it has nothing to do with Java as a language, it's a fraction of the security bugs that affected the web for the last decades (and it's also pretty dead).
The majority (by far) of thoses bugs were in fact memory issues[1], which would have been solved from the beginning if the browsers (or the plugins, like Flash) were written in Java (or Rust).
[1]: https://hacks.mozilla.org/2019/02/rewriting-a-browser-compon...
That's not even absolutely correct, because provability depends on correct preconditions, and there is a universe of incorrect preconditions that can throw a wrench into your provable system.
Your completely 'proven' system fails, when there's a short in jump resistor J35 on the motherboard, which invalidates the entire underpinning of the software system you assume to have.
For critical systems, you should strive for resiliency and failovers, not provability.
Apparently there’s a way to have code execute at compile time (as an alternative to preprocessor macros I guess) and there’s some API in the compiler that either lets you change the grammar or interact with the AST in some way. This guy was using it to add something to the syntax (I think) to make something easier. It sounded awesome (this guy was always doing some of the neatest stuff for fun) but my immediate thought was “now you can’t really be sure other parts of the code aren’t doing this and that severly limits the assumptions you can make about code you’re interacting with.”
I probably should play with it anyway since they certainly have some neat ideas but some of the complexity the language allows sounds really bad for larger projects.
I took a quick look, and all of them seem to be for calling functions from the C library. Moreover, nearly all of these seem to be system calls (open, close, select, ioctl...). Since the Rust compiler can't verify that these calls are safe (for an obvious example: the "read" call can write to arbitrary memory), it requires an "unsafe" block for them - signalling that it's the programmer's responsibility to make sure it's safe (in the "read" example, receiving a "&mut [u8]" from outside the "unsafe" block, and passing that slice's pointer and length as the parameters to the "read" call, would be safe).
The crate also has a nice design decision to have a dedicated module for using these FFI functions, and unsafe is only allowed in this module. The rest of the crate errors if it has any unsafe code.
It's "safe" at the lowest level. No information after that.
This is not at all the point.
So maybe your IP stack has a bug, but at least you won't have a remotely exploitable vulnerability. At least 3/4rd of all CVEs are due to memory safety. Its 2019, why does this need pointing out.
The fortunate thing is that, in practice, you can program in Rust for years without ever needing to write an `unsafe` keyword, depending on your chosen domain. `unsafe` is mostly useful for calling external C code, for implementing maximally-efficient custom data structures, and interfacing with hardware.
It's easy to game, but with reasonably written code the number of lines in unsafe blocks should correlate fairly strongly with the number of times you did something scary. (Scary here includes things as basic as calling a c function or system call directly)
But I can't find any reference to this now. Does anyone else remember this?
Is this true? Can you interoperate with the Internet by just implementing the spec?
The real problem is that every step toward 100% gets progressively harder to find and debug. See e.g. [0] for a middle box that would mangle connections if it received two identical SYN packets, or [1] for a way in which almost all servers anyone currently runs are accidentally resilient to a certain kind of connection-killing packet corruption, but S3 isn't.
[0] https://www.snellman.net/blog/archive/2014-11-11-tcp-is-hard... [1] https://www.snellman.net/blog/archive/2017-07-20-s3-mystery/
As an alternative perspective... while the RFCs are great, it's taken me weeks (maybe months now) to hack together a not-yet-functional TCP/IP stack. I've never dug below the application layer before. I'm still working through it. I cannot even say if the RFCs are sufficient, but I'll take your word.
You can probably get interoperability at least part of the time. You may have deteriorated throughput and more broken connections with some stack and not with others. But you have introduced a new species. If your brand new stack has a tiny bug or new kind of misconfiguration and you start spreading it fast, the hell can break loose and you may ruin the day of many people before they find you and cut you off.
By using this collection of TCP/IP RFCs that grew over the years, it is indeed possible to implement a stack from first principles and have it interoperate with other existing stacks without much trouble. (At least so long as you don't put the same bugs in your test suite as you do in your stack... which you will.)
However, being able to transmit some bytes reliably, and having a high-performance stack that works well in real world conditions are different. You might be able to do the former from RFCs, but the latter absolutely requires a nontrivial amount of tribal knowledge that you have to collect crumb by crumb, and often quite painfully, too.
Smoltcp is somewhere halfway between. It's pretty reliable, but I am sure there is much to be improved in its operation in adverse conditions, with obscure peers, and so on.
I've never implemented the entire stack. Few people have. But from the protocols I've implemented, that's only a slight exaggeration.
As soon as a popular implementation has a bug or decides not to behave according to spec, everyone has to adapt. You can typically use the spec to get 95% of the way to full interoperability.
Also, one of the RFCs has wrong functions for calculating or adjusting checksums. I think there's also some convention on tcp option ordering that may be important but not well documented.
Either way, I would keep a couple other implementations close -- if not to peek at their code ocassionaly, at least to inspect their output.
Why? That sounds more like an ideological decision than a pragmatic, engineering-driven one. Especially for a TCP/IP stack, where performance is typically a major concern, be it in a desktop, server, or embedded environment.
So I made the decision to do the simplest possible thing: write the packet serializers/deserializers entirely by hand. It took very little time and adding any new features was easy and predictable. I believe it was the right decision as it allowed me to focus on the hard parts of a TCP/IP stack.
For a new stack, though, correctness and readability are more important than performance.
It works with tun/tap interfaces and there's a tcpdump clone in the example using raw sockets that works on any regular Linux network interfaces.
Pretty cool!
https://github.com/ANLAB-KAIST/usnet_sockets → Socket library for Rust using smoltcp as userspace network stack (provides types compatible with the standard library, tokio is unpublished WIP)
https://github.com/ANLAB-KAIST/usnetd → Memory-safe L4 Switch for Userspace Network Stacks (firewalls the kernel network stack, see Ideas section for alternatives, e.g., transparently piping the kernel network packets through smoltcp)
and
> ARTIQ (Advanced Real-Time Infrastructure for Quantum physics) is a leading-edge control system for quantum information experiments. [2]
smoltcp is also used by Redox, an OS written from scratch in Rust.
My understanding based on that description is that it is meant for applications that run directly on the hardware, without an OS in the middle. I'm thinking embedded applications.
So I'm thinking that this is meant for IoT-style appliances and the like. Maybe I'm wrong :)
[0]: https://github.com/m-labs/smoltcp#hosted-usage-examples
This could be useful for a userspace TCP stack; sometimes it's better to do the whole thing in your process than the kernel.
I may be wrong here and others are more than welcome to correct me.
EDIT: Added "or an equivalent"
SmolTCP could (in theory) replace the implementation packaged with an OS, or even be used completely from userspace by taking over the raw network interface.
I didn't mean that this exact implementation is shipped with every OS. I meant an equivalent of it being shipped with OSes.
Wit that, don't bother with it so far. That's a toy. It would be dangerous on any real network. If you doubt it please read up on "congestion collapse"
The only uses of libc are in the TUN/TAP driver, which is necessary if you want to bind it to a virtual OS interface.
Don’t get me wrong, from first look it seems like a perfectly fine product, but the name is atrocious.
Don’t get me wrong, from first look it seems like a perfectly fine comment, but the attitude is atrocious.
It reminds me of the words "cromulent"[2] and "embiggen"[3]; coined by The Simpsons but used in everything from BBC articles to scientific research.
[0]: https://en.wikipedia.org/wiki/DoggoLingo
[1]: https://github.com/aras-p/smol-v
popularized by the simpsons, at least cromulent was a valid english word before the episode, I don't think embiggen was.
From Wiktionary: "A humorous, intentionally morphologically opaque neologism coined by American television writer David X. Cohen for 'Lisa the Iconoclast', a 1996 episode of the animated sitcom The Simpsons."
From Merriam-Webster: "It is safe to say that The Simpsons has contributed a great deal to the English language. One famous example is cromulent, which was coined specifically for the 1996 episode 'Lisa the Iconoclast'."