Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setjmp/longjmp gives unexpected results #21810

Closed
Ratakor opened this issue Oct 26, 2024 · 10 comments
Closed

setjmp/longjmp gives unexpected results #21810

Ratakor opened this issue Oct 26, 2024 · 10 comments
Labels
question No questions on the issue tracker, please.

Comments

@Ratakor
Copy link
Contributor

Ratakor commented Oct 26, 2024

Zig Version

0.14.0-dev.2034+56996a280

Steps to Reproduce and Observed Behavior

I have 2 files that does the same thing, one is in C and uses volatile values, the other is in zig.

test.c:

#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>

sigjmp_buf env;

void alarm_handler(int dummy)
{
	alarm(1);
	signal(SIGALRM, alarm_handler);
	siglongjmp(env, 1);
}

void enumerate_primes(int volatile *current_test, int volatile *largest_prime)
{
	while(1) {
		int i = 2;
		while (i * i <= *current_test && *current_test % i != 0) {
			i++;
		}
		if (*current_test % i != 0) {
			*largest_prime = *current_test;
		}
		*current_test = *current_test + 1;
	}
}

int main(void)
{
	volatile int test = 2;
	volatile int largest_prime = 2;
	volatile int time = 0;

	signal(SIGALRM, alarm_handler);
	alarm(1);

	time += sigsetjmp(env, 1);
	printf("%4d   Largest Prime: %10d\n", time, largest_prime);
	enumerate_primes(&test, &largest_prime);
}

test.zig

const std = @import("std");
const c = @cImport({
    @cInclude("setjmp.h");
    @cInclude("signal.h");
});

var env = std.mem.zeroes(c.sigjmp_buf);

fn alarmHandler(_: c_int) callconv(.c) void {
    _ = std.c.alarm(1);
    _ = c.signal(std.posix.SIG.ALRM, alarmHandler);
    c.siglongjmp(&env, 1);
}

fn enumeratePrimes(current: *volatile u32, largest: *volatile u32) void {
    while (true) {
        var i: u32 = 2;
        while (i * i <= current.* and current.* % i != 0) {
            i += 1;
        }
        if (current.* % i != 0) {
            largest.* = current.*;
        }
        current.* += 1;
    }
}

pub fn main() !void {
    var current: u32 = 2;
    var largest: u32 = 2;
    var time: c_int = 0;

    _ = c.signal(std.posix.SIG.ALRM, alarmHandler);
    _ = std.c.alarm(1);

    time += c.sigsetjmp(&env, 1);
    std.debug.print("{d: >4}    Largest Prime: {d: >10}\n", .{@as(c_uint, @intCast(time)), largest});
    enumeratePrimes(&current, &largest);
}

C code output with -O0 or -O3

   0   Largest Prime:          2
   1   Largest Prime:    3938531
   2   Largest Prime:    6435973
   ...

Zig code output with -ODebug

   0   Largest Prime:          2
   1   Largest Prime:    2146043
   1   Largest Prime:    3518323
   ...

Zig code output with -OReleaseFast

   0   Largest Prime:          2
   1   Largest Prime:          2
   1   Largest Prime:          2
   ...

In zig everything gets optimized even in Debug build! and there is no way to prevent it from being optimized, I tried to cast values to volatile ptr (may have failed because of #21033), use memory barrier, etc... but nothing works.

Note that this also highlights miscompilation between zig cc and zig translate-c |> zig build-exe.

% zig cc test.c -O3 -o test && ./test
   0   Largest Prime:          2
   1   Largest Prime:    3938531
   2   Largest Prime:    6435973
   3   Largest Prime:    8576713
^C
% zig translate-c test.c -lc > out.zig && zig build-exe out.zig -OReleaseFast -lc && ./out
   0   Largest Prime:          2
   1   Largest Prime:          2
   1   Largest Prime:          2
   1   Largest Prime:          2
^C

Expected Behavior

Have a way to actually prevent code to get optimized away, this is a real problem in embedded/kernel development or, for example, lightweight user space threads implementation.

@Ratakor Ratakor added the bug Observed behavior contradicts documented or intended behavior label Oct 26, 2024
@alexrp
Copy link
Member

alexrp commented Oct 26, 2024

I tried to cast values to volatile ptr (may have failed because of #21033)

Can you specify exactly what you did there?

@Ratakor
Copy link
Contributor Author

Ratakor commented Oct 26, 2024

I tried to cast values to volatile ptr (may have failed because of #21033)

Can you specify exactly what you did there?

I did this, which doesn't change anything even in Debug build.

time +=
        @as(c_int, @intCast(
        @as(isize, @bitCast(
        @as(usize, @intFromPtr(
        @as(*volatile allowzero anyopaque, @ptrFromInt(@as(usize,
        @bitCast(@as(isize,
            c.sigsetjmp(&sigjmp_buf, 1))))))))))));

And I also tried this which doesn't work either in Debug build.

@as(*volatile c_int, @ptrCast(&time)).* += c.sigsetjmp(&sigjmp_buf, 1);

On a side note, separating the assignment produces the good output in Debug build.

const rv = c.sigsetjmp(&sigjmp_buf, 1);
time += rv;

@rohlem
Copy link
Contributor

rohlem commented Oct 26, 2024

The main difference I see between the two versions is that in the C code the variables in main are each declared with volatile and in the Zig code they aren't.
I assume making the variables non-volatile in C also results in everything being optimized out / stagnant output of the same values?

And I also tried this which doesn't work either in Debug build.

@as(*volatile c_int, @ptrCast(&time)).* += c.sigsetjmp(&sigjmp_buf, 1);

I assume this attempt didn't work because the read of largest for the print output is still via the non-volatile variable.
Since Zig doesn't have volatile locations (variables), only volatile pointers,
IMO the correct way to express this should be to declare the variables as you do, then declare volatile pointers to these variables,
and only ever access (both read and write) via these pointers:

    // don't access these directly
    var current_storage: u32 = 2;
    var largest: u32 = 2;
    var time: c_int = 0;
    // only access via these volatile pointers
    const current_pointer: *volatile u32 = &current_storage;
    const largest_pointer: *volatile u32 = &largest;
    const time_pointer: *volatile c_int = &time;

    _ = c.signal(std.posix.SIG.ALRM, alarmHandler);
    _ = std.c.alarm(1);

    time_pointer.* += c.sigsetjmp(&env, 1);
    std.debug.print("{d: >4}    Largest Prime: {d: >10}\n", .{@as(c_uint, @intCast(time_pointer.*)), largest_pointer.*});
    enumeratePrimes(current_pointer, largest_pointer);

If that still doesn't work, in my understanding that would be a bug.
The fact that translate-c also didn't preserve the semantics of the C program is equally a bug in translate-c.

EDIT: Actually, since setjmp/longjmp aren't "Zig-native", I'm not 100% sure whether Zig's semantics guarantee (volatile_pointer).* += to be reentrant-safe in this regard - the language might allow caching the read of 0 before adding the function call result.
However, even if Zig "natively" chooses to understand this differently, translate-c requires a workaround to model the behavior correctly. (I.e. if it doesn't, that is a bug in translate-c that needs to be fixed.)

@Ratakor
Copy link
Contributor Author

Ratakor commented Oct 26, 2024

I assume making the variables non-volatile in C also results in every output being 2?

Removing all volatiles in the C code still produce good output with -O0, whatever compiler is used.

I assume this attempt didn't work because the read of largest for the print output is still via the non-volatile variable.

No, I tried it with

std.debug.print("{d: >4}    Largest Prime: {d: >10}\n", .{@as(*volatile c_int, &time).*, @as(*volatile u32, &largest).*});

About volatile pointers, it still doesn't work and I think it may be related to #21033.


Side note about setjmp/longjmp, this issue is not related to these functions in particular. I used them because it's easier for a repro but the same kind of behavior can happen in an environment with non-local jump e.g. kernel interruptions.

@mlugg
Copy link
Member

mlugg commented Oct 26, 2024

setjmp/longjmp is unsound in Zig; see #1656.

If you have a valid repro, please open another issue for it.

@mlugg mlugg changed the title Lack of volatile values & miscompilation setjmp/longjmp gives unexpected results Oct 26, 2024
@mlugg mlugg closed this as not planned Won't fix, can't repro, duplicate, stale Oct 26, 2024
@mlugg mlugg added question No questions on the issue tracker, please. and removed bug Observed behavior contradicts documented or intended behavior labels Oct 26, 2024
@Ratakor
Copy link
Contributor Author

Ratakor commented Oct 26, 2024

I will try to make another repro with qemu but this is still a miscompilation between zig translate-c and zig cc, it's not a question it's a bug...

@mlugg
Copy link
Member

mlugg commented Oct 26, 2024

translate-c will not correctly translate all C code; setjmp is not supported in Zig, and code using it cannot be correctly translated. This is not a bug.

@rohlem
Copy link
Contributor

rohlem commented Oct 26, 2024

Translating C code into Zig code with silently/subtly different behavior is suboptimal behavior.
If at all possible, code using setjmp and longjmp should result in a compile error (f.e. be translated to @compileError stating the issue).
Since Zig can be compiled together with C code though, and one of the main use cases for Zig is integration with C, imo we should figure out some strategy/workaround for this scenario,
even if it may be keeping the affected function implementations in a C compilation unit and calling them via extern fn.

@Ratakor
Copy link
Contributor Author

Ratakor commented Oct 26, 2024

translate-c will not correctly translate all C code

How do you plan to ditch clang for #20875 then?

@rohlem
Copy link
Contributor

rohlem commented Oct 26, 2024

@Ratakor I don't think that is necessarily related - in the plan laid out in #20875 , instead of invoking zig cc a project would depend on a clang+llvm zig package that provides the same functionality.
For Zig interacting with C code, the zig compiler can always emit LLVM bytecode (or is it called bitcode?) and provide that as input for the LLVM compilation pipeline (whether integrated in the zig binary or in a separate zig package).
translate-c is specifically for translating C code to Zig code, which is not necessary in the compilation process of a Zig, C, or combined project. Its primary use is during development.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question No questions on the issue tracker, please.
Projects
None yet
Development

No branches or pull requests

4 participants