I am reading the documentation and it says:
Most Illegal Behavior is safety-checked. However, to facilitate optimizations, safety checks are disabled by default in the ReleaseFast and ReleaseSmall optimization modes.
Why does it say most here? Is there any illegal behavior that is never safety checked? Even in debug/releaseSafe builds? Is this just because the @setRuntimeSafety macro and inline assembly can result in unchecked illegal behavior? I’m assuming all of the illegal behavior listed in the Illegal behavior section is safety checked in debug and releaseSafe builds?
Thanks
.
1 Like
Yes, there is plenty, that range from: solved but not yet implemented, don’t yet have a solution that fits zig, known solution is difficult and slow, or not adequate, to never been solved before ever.
First that comes to mind is pointer casts, they only have 2 checks: alignment (via a separate cast) and size (only applied with slices).
Any cast to/from a type without a defined layout is illegal because you don’t know what data is where.
Well It’s using it after that is the illegal part, and you can go around that by casting back to the correct type. That allows for runtime interfaces to be legal.
There are plans to add unique values into types that don’t have a defined layout that can then be checked when casting, and ofc zig will provide a way around that as it could get in the way of some legal things.
Another pointer related IB that comes to mind is incorrect use of @constCast; if the pointer points to memory that is supposed to be const, and you write to it, that is unchecked illegal behaviour.
8 Likes
Searching the language reference for “unchecked” reveals an example.
var declarations inside functions are stored in the function’s stack frame. Once a function returns, any Pointers to variables in the function’s stack frame become invalid references, and dereferencing them becomes unchecked Illegal Behavior.
Here is a demonstration:
fn obfuscate(arg: *u8) *u8 {
return arg;
}
fn buggyFn(x: u8) *u8 {
var stack_allocated: u8 = x;
return obfuscate(&stack_allocated);
}
test "fails because of unchecked IB" {
const ptr: *u8 = buggyFn('a');
std.debug.print("mess up the stack\n", .{});
try std.testing.expectEqual('a', ptr.*);
}
2 Likes
Thanks guys, in hindsight these are all obvious examples of unchecked behavior lol.
3 Likes
An infamous example is that you define a slice without heap allocated inside a function, and then the function returns something related to the slice… A lot of people do that for string…
Even the ReleasedSafe doesn’t catch that. When you call the function and access to the returned result, you access to an already freed address, because the slice is a stack, and it is inside the scope of the function. When the function is called and completed, the slice is freed.
A similar example is that some people allocated a struct inside a function and defer free it inside a function… The struct has some fields of i32, f64, and []u8… You can return the fields of i32 and f64 because you return the values… However, when you return the []u8, there is no warning nor error catched by the ReleaseSafe… And sometime, you still get the right string because the freed addresses haven’t been reused yet…
Even the ReleaseSafe still has a long way to go to catch all the funny things that people doing.
1 Like