Hacker News new | ask | show | jobs
by mlindner 1514 days ago
I'm confused when people preach Zig and then safety in the same sentence. Zig has basically all the same problems that C++ has with a few extra bells and whistles. There are perfectly good reasons to like Zig, but it being "safe" is not one of them.

> For example, this comes through in little things like Zig's extreme simplicity and explicitness

"Simplicity" does not mean safety. In fact simplicity means you can't describe hard problems easily without being overly verbose, which means more code and more attack surfaces. Problems are what can be simple or complex. If your language advertises "simplicity" it just means it can't solve complex problems in straight forward ways. Zig is just repeating the mistakes of C++ as well as all the mistakes of Go.

> or people who haven't worked in security, integer overflow and wraparound may seem like a small thing, but they increase the probability of buffer bleeds, i.e. attacks like OpenSSL's Heartbleed, which are often remotely accessible and easier to pull off than a UAF.

Zig does exactly what Rust does in this case.

> In fact, no language is 100% memory safe, i.e. able to prevent a memory buffer bleed, because these are logic errors with respect to the file or protocol format.

What are you trying to argue here? Buffer bleeds can't happen in safe rust.

> The borrow checker is obviously great for multithreaded systems, but if you're using io_uring for fast I/O, then multithreading is less of the necessary evil that it used to be a few years ago.

Syncronization is a problem in any kind of multitasking. Whether that happens inside or outside the language is the only question. The OS can have all the same problems your code can have. https://mobile.twitter.com/axboe/status/1505335772706091011 Also io_uring assumes you're only ever writing for Linux.

> Nevertheless, Zig is safer than Rust when it comes to checked arithmetic or being able to handle memory allocation failure.

This is simply incorrect. Most of the problem with unchecked overflow is for buffer overflows, which are fully caught. Rust gives options to handle memory allocation failure if you want to handle it. Generally though for most applications simply crashing is completely fine.

4 comments

> I'm confused

Thanks, I've tried to clarify that Zig provides spatial memory safety but not temporal memory safety, so hopefully it's less confusing.

> "Simplicity" does not mean safety.

Ceteris parabus, complexity breeds bugs and simplicity improves the probability of safety. For example, if I were auditing a piece of code for security, I would prefer the simplest correct program to the most complex correct program. Reduced surface area means reduced area for attack.

When I say that Zig pursues simplicity, I also mean this as high praise, that Zig is highly "orthogonal". In other words, able to solve difficult problems with a minimum of overlapping features. For example, Zig's comptime gives you generics, but also gives you so much more, plus there is also type safety throughout Zig's comptime, yet it eliminates the need for macros, and is more versatile, powerful and flexible at the same time. It's incredibly elegant. Nothing left to add, nothing left to take away.

> Zig does exactly what Rust does in this case.

Things like checked arithmetic matter and should be enabled by default in safe builds, yet Rust does not actually do this in safe release builds. Zig does and I hope that Rust one day will.

> What are you trying to argue here? Buffer bleeds can't happen in safe rust.

By definition, buffer bleeds can in fact happen in safe Rust. The borrow checker can protect against UAF and overflow, but the borrow checker can't protect against all kinds of underflow, which is what a buffer bleed is. You can even pull them off in JavaScript.

No language is actually 100% memory safe, not with respect to buffer bleeds.

> Things like checked arithmetic matter and should be enabled by default in safe builds, yet Rust does not actually do this in safe release builds. Zig does and I hope that Rust one day will.

The entire origin of unchecked arithmetic being a problem originates from their use as indexes to buffers. If you solve the indexing buffer issue you don't need to completely expand every single mathematical operation to a checked one which slows down the code. If people want checked artihmetic for some reason in the rare case that it matters in a non-buffer case, then they can used things like checked_add.

> Reduced surface area means reduced area for attack.

First of all, that's security, not safety. Lots of software exists in an environment where it isn't going to be attacked.

> Things like checked arithmetic matter and should be enabled by default in safe builds.

Rust offers actual checked arithmetic - for example, suppose I have a 32-bit signed integer (i32) and I want to add something to it, in Rust I can choose the most appropriate of:

add - also presented as the + operator, in debug this will panic on overflow, but in production I can choose, either panic or just have it wrap silently

checked_add - at runtime check for overflow and get either None or Some(i32)

overflowing_add - this time instead of Option we get a pair back, with our (wrapped if necessary) numeric answer and a boolean saying if we overflowed

saturating_add - addition saturates at the minimum and maximum thresholds

unchecked_add - even in debug builds this is not checked (yes it's unsafe)

wrapping_add - this performs the most likely native hardware behaviour, wrapping numbers around at the limits

You might also realise your variable should inherently have certain arithmetic, for example a 16-bit PCM sample should always have saturating arithmetic (getting this wrong is one reason badly written older audio software can sound bad) so you can make them Saturating<i16> or maybe you actually want wrapping arithmetic on the simulated 32-bit CPU registers in your Motorola 68000 simulator, so you use Wrapping<u32> for them.

The debug behaviour for the trivial + operator is not intended to be your best defence, if you're sure you want wrapping, write that down, if you're sure you want to check, write the check. But sure, if you somehow want your release code to panic, but don't want to write the panics out by hand, you can tell the compiler you want this in release builds.

> By definition, buffer bleeds can happen in safe Rust.

Nope.

> The borrow checker can protect against UAF and overflow, but it can't protect against all kinds of underflow

And it doesn't, in safe Rust the actual bounds checks are emitted, and they only get elided in most cases because in idiomatic Rust it's clear to the compiler that the checks are unnecessary (e.g. iterating over items in a vector, the compiler knows how long the vector is, and it can see we're starting at the beginning and stopping at the end, so, we don't need the bounds check and it won't actually be emitted in the machine code). thing[index] is bounds checked in safe Rust.

> No language is actually 100% memory safe, not with respect to buffer bleeds.

Many languages are 100% memory safe. Most fascinating here is WUFFS which - in exchange for its restricted purpose - gets to be both entirely safe and faster than the C (or C++, or Rust) you'd actually write.

You literally can't write a "buffer bleed" type goof in WUFFS. I don't mean "You won't because it's so easy to get it right" or even "It will warn you about the problem at runtime so you can fix it". I mean code which can exhibit that bug does not compile.

> First of all, that's security, not safety. Lots of software exists in an environment where it isn't going to be attacked.

I use the terms interchangeably on purpose, because safety and security are really two sides of the same coin. In the past, I've worked in security and I now work in a domain where safety is critical. It's my experience that the two fields have considerable overlap.

We can always get better at interdisciplinary sharing of knowledge.

> I'm confused when people preach Zig and then safety in the same sentence.

Zig enforces correctness much more than C or C++ does (e.g. no implicit 'lossy' conversions, much stricter and enforced error handling, no implicit null, etc.), so it's completely fine to say that Zig is much safer than C or C++.

The main difference between Zig and Rust is the borrow checker, but a lot of memory corruption issues in C and C++ are "second order effects" of sloppy code which would be caught in Zig either at compile or runtime.

Except the little detail about sloppy use after free.
Agreed but doing this at compile time would require some sort of pointer lifetime tracking in the compiler. Don't know how much of this is feasable without going "full borrow checker", but static analyzers for C do a similar thing, by far not as watertight as Rust of course.

The General Purpose Allocator can optionally catch use-after-free by not recycling virtual memory addresses, not very efficient for small high-frequency allocations of course, but then Zig isn't an OOP language either (where it would be common to create and destroy massive amounts of tiny objects).

> ...but static analyzers for C do a similar thing..

Exactly, which begs the question why bother with Zig at all.

Zig is a much more ergonomic language, toolchain and build system. And since Zig is also a C/C++/ObjC toolchain, it's not about "Zig or C/C++", but about "Zig and C/C++" (at least from my PoV).
From my point of view it isn't enough to warrant adoption over existing alternatives.

Maybe someone proves me wrong with some killer application that makes Zig unavoidable for the rest of us.

Zig is a competitor to C, I think that really clarifies its design decisions. Eg., It's use of a interpreter during compilation to process macros, redresses C's purely textual approach.
> In fact simplicity means you can't describe hard problems easily without being overly verbose, which means more code and more attack surfaces.

Hence simplicity is not a desirable goal. We need to create complex languages so that fewer people can understand and use it in a few projects and write very less code. Less code means less bugs. ‘Complex’ is the way to go. /s