Skip to content

Share a canonical panic handler across compilation units #20240

@mlugg

Description

@mlugg

Whilst looking into porting the UBSan runtime to Zig, I recalled a conversation with Andrew about the fact that @panic ought to only result in a single panic handler across all compilation units in a binary. Ideally, it should be possible to both reference the handler from C code, and define it in C code.

The self-hosted compiler does not yet implement this behavior: uses of @panic just emit calls to std.builtin.panic. This issue details a proposal for how to implement this behavior.


The panic handler is provided by a function with the following signature (note that this depends on #17969):

/// The panic cause is unpacked into `cause`, `data0`, and `data1`; depending on `cause`, the data parameters may be ignored.
/// If there is no error return trace, then `error_return_trace.index == std.math.maxInt(usize)`.
/// If there is no return address, then `ret_addr == 0`.
extern fn __zig_panic(cause: @typeInfo(PanicCause).Union.tag_type.?, data0: usize, data1: usize, error_return_trace: CompletedStackTrace, ret_addr: u64);

const CompletedStackTrace = extern struct {
    index: usize,
    instruction_addresses: [*]usize,
};

Calls to @panic are translated into calls to this function, converting the PanicCause, ?StackTrace and ?u64 to the parameter types according to the doc comments.

The more difficult question to answer is which compilation unit provides this handler, and how. I propose the following rules.

  • If -fpanic-handler is passed to a compilation, the panic handler is exported from the ZCU if Zig source files are provided. Otherwise, a single compilation unit containing only the panic handler is implicitly added to the compilation. (The resulting object can be cached globally.)
  • If -fno-panic-handler is passed to a compilation, no panic handler is exported.
  • Otherwise, we export the panic handler according to the logic from the -fpanic-handler case, but:
    • If we are building an executable, the panic handler is exported with strong linkage.
    • If we are building an object or library (shared or static), the panic handler is exported with weak linkage.

This system allows the user to set a preference using -f[no-]panic-handler if desired, but otherwise provides reasonable defaults. Let's analyze the possible failure cases:

  • a binary has no panic handler
  • a binary has multiple panic handlers, all with weak linkage
  • a binary has multiple panic handlers, at least two with strong linkage

A binary having no panic handler is not possible under the default settings (it can only happen if -fno-panic-handler is passed to every compilation). In this case, a link error occurs if any CU references the panic handler, but it is due to explicit user overrides preventing successful compilation, which seems like a non-issue.

A binary having multiple panic handlers, all with weak linkage, can occur by default if the final executable is not built with Zig - for instance, if a library (static or shared) is built with Zig and linked into a C program which is compiled with clang or gcc. In this case, the linker will functionally choose a random implementation (I think it's technically the first one?). This isn't ideal, but isn't easily resolvable, because no compilation unit is more authoritative than any other in this context. In practice, it will be rare for libraries to override the panic handler, so all will probably have the default, meaning the "randomness" here is typically unobservable. If any CU provides a custom panic handler, then the user can set -fpanic-handler on that one in their build script to give it strong linkage and hence make it override the others.

A binary having multiple panic handlers, at least two of which have strong linkage, can never occur by default. It only happens when -fpanic-handler is passed to at least one compilation. The main case I see being confusing here is passing -fpanic-handler to a library, and linking it to a full executable, all with Zig: both will provide panic handlers with strong linkage, at which point we again defer to functionally random selection. This case is unfortunate: the user presumably expected the override to cause the panic handler to deterministically come from the library. For this reason, we augment the executable rule a little:

  • If we are building an executable, the panic handler is exported with strong linkage, unless another link object already provides it with strong linkage.

This extra case removes this unintuitive behavior, whilst still making sure to provide a panic handler in all cases. It makes sure that doing everything with the Zig compiler is a happy case, where everything "just works".


Let us now look at the impact of this design in practice. I'll list a few scenarios, and explain how the logic would play out (assuming no -f[no-]panic-handler overrides).

Scenario 1: building an executable, from Zig code, which links to a static library, also written in Zig. In this case, the library exports a panic handler with weak linkage, and the executable overrides it with strong linkage. Thus, as is intuitive, the Zig code providing the entrypoint also provides the panic handler.

Scenario 2: building a shared library written in Zig, which is used by C code. Here, the shared library exports a weak symbol, and the C binary which uses it naturally exports nothing, so the weak symbol from the shared library is used. The C code could provide a strong __zig_panic symbol if it wants; I'm unsure if this would be used in this case (how does the dynamic linker handle weak symbols?).

Scenario 3: building a static library written in Zig, which itself links another static library, also written in Zig. In this case, both handlers have weak linkage, so we get the "random" behavior discussed above. This, of course, assumes that the consumer of the library is compiled with a C compiler; if the final executable is built with Zig, then the executable overrides both panic handlers.

Scenario 4: building an executable written in Zig, with a C compilation unit which provides __zig_panic. In this case, the "unless another link object already provides it with strong linkage" rule kicks in, and the C definition overrides the Zig one. I think this is the intuitive behavior in this case.


The main thing I'm not certain of is how all of this interacts with shared libraries, since I don't know how they interact with weak linkage in general (are the links resolved at link time or load time?). @kubkon, could you shed some light on this?

I'm also unsure if weak linkage is an issue on any target. From what I can see, Windows is a little shaky on it, but does seem to support it in at least some sense -- this will be another @kubkon question I suppose!

Metadata

Metadata

Assignees

No one assigned

    Labels

    breakingImplementing this issue could cause existing code to no longer compile or have different behavior.linkingproposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions