ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.67k stars 2.47k forks source link

Proposal: Introduce safety-checked UB for reading `invalid` bit patterns via `@bitCast`, pointers, and untagged unions #6784

Open rohlem opened 3 years ago

rohlem commented 3 years ago

Status-quo

The Zig type system allows for types that don't cleanly fit into bytes / registers, I'll call them sparse types for short. These have invalid bit patterns, which do not map to a value of the specified type. For example:

The only way to produce invalid values of sparse types is via reinterpretation operations, which include:

As far as I could find, the documentation doesn't state how invalid values are handled in the general case. Instead, every checked, value-preserving cast is individually documented to trigger safety-checked undefined behaviour if the input value is out-of-bounds for the target type.

This proposal

Formally specify that constructing an invalid value is always immediately illegal (safety-checked undefined) behaviour.

I wrote a couple of example scenarios. These conditions are currently not checked (I can create individual issues if the general idea is accepted):

As a special case, stage1 currently always loads all integer types by exactly their bit width (masking all other bits off), even when they have byte alignment (i.e. outside of packed types) and even when runtime safety is disabled. This ensures valid values by hiding the actual invalid bit pattern, potentially hiding semantic errors. I feel like this is also the wrong default, and worth reconsidering.

Update (2023-06-26): Sentinel-terminated arrays [n:s]T are also an interesting case to also consider in this context. (I can't remember if they were in the language when I wrote this proposal originally.)

cajw1 commented 3 years ago

What are the reasons for not "doing something radical" and rejecting all of this at compile time (including untagged unions with sparse fields unless they share the same legal bit patterns)? I suppose I am missing some obvious C interop reasons...

rohlem commented 3 years ago

@cajw1 I'm not personally opposed to these "radical" changes/limitations either. I wanted to clarify that I wouldn't blindly set them in stone up front though, since

cajw1 commented 3 years ago

@rohlem , I had not thought about heap allocation...

Looking at mem.zig and Allocator.zig: Allocator.create(...) is defined to return a pointer to undefined memory (line 300 https://github.com/ziglang/zig/blob/master/lib/std/mem/Allocator.zig). Using this before defining it is UB (though not tracked at compile-time, and I don't understand how the runtime tracks whether a u8 is in state undefined, or what memsetting a u8 to undefined actually does, understanding https://github.com/ziglang/zig/issues/4298 might help but I so far fail to understand why/how undefined should/could be a runtime concept).

What if @ptrCast(SparseType, valueOfLessSparseType) somehow were defined to return a pointer to undefined memory and :) the compiler ensured that undefineds are always defined before use? EDIT: see @rohlem https://github.com/ziglang/zig/issues/6784#issuecomment-716456775, this proposal would be better formulated as: What if @ptrCast(SparseType, valueOfLessSparseType) somehow were defined to return a pointer to invalid memory and :) the compiler ensured that invalids are always defined before use?

As soon as you cast a non-sparse to a sparse, you are really overpromising, so if such a cast were considered to undefine the cast memory (only as a compile-time flag, no runtime effect), that promise would go away. If the only use-cases for this were such that a compile-time-enforced define after such a cast were acceptable (I am only thinking of the malloc use-case, where it is ok if the only thing you can do after such a cast is a define)... MMIO (and optimal deserialization etc) will have to use non-sparse types (I suspect extern struct is enforcing this already, but packed structs are not, they accept sparse types as members and can take part in ptrCasts).

rohlem commented 3 years ago

Just to be clear, this proposal is (intentionally) not about undefined, but the concept of invalid values from other sources. undefined could also be considered such a source, but currently has many implications beyond that.

I'd like not to sidetrack too much, but to quickly address your ideas @cajw1 :

I'd really appreciate more feedback from more knowledgeable developers than us, but if this is deemed the right direction, then a type distinction * maybe-invalid T (as proto-proposed by @cajw1 in https://github.com/ziglang/zig/issues/3328#issuecomment-715902461 ) might be the right avenue. But I think that's best discussed in its own proposal/issue, since @ptrCast is only the fourth bullet point of this more general proposal. EDIT: changed * maybe-undefined T to * maybe-invalid T, because reasoning about undefined values (even to check if they're invalid) is not supported by current semantics, and I wouldn't want to involve undefined in the general discussion of invalid values.

kyle-github commented 3 years ago

Introducing a second concept of invalid seems interesting. I had not realized what LLVM does with undefined. At least to me, that makes use of undefined fairly undesirable since you do not really know what the LLVM machinery is going to do with it :frowning_face:. It sounds like undefined is closer to a synonym for unused.

Would invalid actually need to have a keyword? Perhaps the compiler could assign every variable invalid status unless proven otherwise by a valid assignment/initialization? The challenge then becomes determining what a valid assignment/init is.

Valid:

Possibly invalid:

Unfortunately, I do not see how you will get valid data from an allocator. Maybe you also pass in an initialization function to the allocator? This would take an OO constructor and turn it inside out.

Sorry for the incoherent rambling. Clearly I have not had enough caffeine yet. It does seems like this concept of valid vs. invalid/unproven valid is possibly very powerful and probably different from undefined.

rohlem commented 3 years ago

@kyle-github I think a keyword (type attribute) is necessary as soon as it affects interfaces. To quote the example from https://github.com/ziglang/zig/issues/3328#issuecomment-715902461, output parameter pointers (f.e. @addWithOverflow) will (in case of success) overwrite the value pointed to, so they could declare these as * maybeinvalid T. The compiler internally handling this logic might be viable, but maybe code dealing with pointers ends up as more readable if we allow it this expressiveness.

Either way, I'd like for this proposal to remain about the idea that "we want a panic (safety-checked UB) when we cast to /read invalid values", and not conflate it with solutions for dealing with invalid values (which needs to be done before they are cast to /read).