[RFC] Emit a warning when -fsanitize-trap=<...> is passed without associated -fsanitize=<...>

Introduction

-fsanitize-trap= is an Undefined Behavior Sanitizer (UBSan) flag that enables trapping UBSan, which causes programs built in this mode to execute a trap instruction if certain undefined behavior is observed. This is a “lighter weight” version of UBSan because it has no associated runtime (unlike userspace UBSan) that makes it suitable for deployment in environments that do not have (or do not want) a UBSan runtime.

A problem that currently exists with trapping UBSan is if the -fsanitize-trap=<sanitizer> flag is provided to the Clang driver but -fsanitize=<sanitizer> is not then this configuration of compiler flags silently does nothing.

Motivation

We do not think the current behavior of silently ignoring -fsanitize-trap= when it appears on its own is the right behavior. Typically the Clang driver warns about unused flags.

E.g.:

Passing linker arguments without initiating a linking phase ([5][6]):

$ clang -c foo.cpp -la

clang: warning: -la: 'linker' input unused [-Wunused-command-line-argument]
$ clang -c foo.cpp -rdynamic

clang: warning: argument unused during compilation: '-rdynamic' [-Wunused-command-line-argument]

Passing architecture specific flags with the wrong target ([7][8]):

$ clang --target=x86_64-apple-darwin -c foo.cpp -mthumb

clang: warning: argument unused during compilation: '-mthumb' [-Wunused-command-line-argument]

The current behavior aside from being at odds with the typical behavior of Clang makes it more likely a user of Clang will make a mistake by thinking they have UBSan instrumentation enabled when in fact, they don’t. There are internal reports inside of Apple that users of Clang found the current behavior confusing.

Proposed Change

We propose emitting a warning diagnostic when this occurs in order to prevent the accidental omission of the necessary flag to utilize -fsanitize-trap=<...> functionality. The warning would be on by default but will be placed in a warning group which allows it to be disabled for projects who wish to have the current behavior (i.e. no warning).

$ clang -fsanitize-trap=undefined test.c

clang: warning: -fsanitize-trap=undefined has no effect because the "undefined" sanitizer is disabled; consider passing "-fsanitize=undefined" to enable the sanitizer
$ clang -fsanitize=undefined -fsanitize-trap=undefined test.c

<no warning>
$ clang -fsanitize-trap=undefined -Wno-sanitize-trap-mismatch test.c

<no warning>

Projects relying on the current behavior

We have received feedback ([1][2][3]) that the current design is intentional and allows something like this.

Project(TestUBSan C)
cmake_minimum_required(VERSION 3.24)

# Enable UBSan at the top-level
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=undefined")

option(UBSAN_TRAPS ON)

if(UBSAN_TRAPS)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize-trap=undefined")
endif()

# Some targets use UBSan
add_executable(my_program_1 test1.c)

# We need to opt some targets out.
add_executable(my_program_2 test2.c)
target_compile_options(my_program_2 PRIVATE "-fno-sanitize=all")

Here

  • my_program_1 builds with -fsanitize=undefined -fsanitize-trap=undefined
  • my_program_2 builds with -fsanitize=undefined -fsanitize-trap=undefined -fno-sanitize=all

Under this RFC when building my_program_2 a new warning will appear. Previously there would be no warning.

warning: -fsanitize-trap=undefined has no effect because the "undefined" sanitizer is disabled; consider passing "-fsanitize=undefined" to enable the sanitizer

We do not believe our RFC is at odds with build systems that want to do this style of UBSan opt out. Under the changes proposed by this RFC the project can continue to build with the new warnings appearing (as they are non-fatal) or they can hide the warnings by suppressing them.

target_compile_options(my_program_2 PRIVATE

"-fno-sanitize=all -Wno-sanitize-trap-mismatch")

It has been suggested that the current behavior is desirable and builds systems like the above justify this. While we have sympathy for this view we believe:

  • The current behavior is at odds with how Clang normally handles unused driver flags
  • Prioritizing the needs of a particular build system over the general usability of a Clang feature does not seem like the right trade-off. Especially because existing build systems can very easily adapt to this change.

Implementation

As part of my 2025 Google Summer of Code project I have implemented a sketch patch showing how this could be implemented. It turns out this was also implemented before in [4] but the effort stalled.

Unresolved problems

Object-size sanitizer has non-obvious behavior

At -O0, Clang intentionally disables the -fsanitize=object-size sanitizer even though the user may have enabled the sanitizer group, -fsanitize=undefined, which -fsanitize=object-size is a part of.

Example with the sketch patch implemented:

$ clang -O0 -fsanitize=undefined -fsanitize-trap=undefined foo.c

warning: -fsanitize-trap=object-size has no effect because the "object-size" sanitizer is disabled; consider passing "-fsanitize=object-size" to enable the sanitizer
$ clang -O1 -fsanitize=undefined -fsanitize-trap=undefined foo.c

<no warning>

Given that this behavior is intentional one possible fix here would be modify the warning to not warn about this particular case.

Alternatively we could require the user to pass -fno-sanitize-trap=object-size

E.g.:

$ clang -O0 -fsanitize=undefined -fsanitize-trap=undefined -fno-sanitize-trap=object-size foo.c

This solution doesn’t seem ideal because the most common case (using the “undefined” group) produces a “technically correct” but confusing warning.

Broken tests

No attempt has been made to make all the tests pass. Some currently don’t because they don’t expect to see the new warning. This can easily be resolved in a complete implementation of the patch. The tests have not been fixed in the sketch patch to make the patch easier to understand and to avoid spending effort before we know the direction that this RFC will take.

Path forward

We would like to get community consensus on the right path forward here. It’s clear there is disagreement hence we have put up this RFC to try to get official consensus.

CC: @delcypher, @Michael137 , @vitalybuka, @MaskRay, @thurston, @usama, @fmayer, @ilovepi, @jyknight, @petrhosek

External Links

[1] [clang][GSoC 2025] Usability Improvements for trapping Undefined Behavior Sanitizer - #5 by jyknight

[2] [WIP][DO NOT MERGE][Clang][Driver] Emit warning when -fsanitize-trap=<...> is passed without associated -fsanitize=<...> by anthonyhatran · Pull Request #147997 · llvm/llvm-project · GitHub

[3] Ubsan: warn on -fsanitize-trap=undefined ignored when passed on its own by clingfei · Pull Request #132319 · llvm/llvm-project · GitHub

[4] ⚙ D101505 [Driver][sanitizers] Warn about ignored -fsanitize-trap or -fsanitize-recover

[5] c++ - clang: warning: argument unused during compilation: '-rdynamic' - Stack Overflow

[6] Clang -Wunused-command-line-argument | MaskRay

[7] Better Firmware with LLVM/Clang | Interrupt - #5 by kisielk - Blog - Interrupt

[8] Reddit - The heart of the internet

1 Like

The PR discussion linked in [2] also talks about compatibility with GCC, which is not discussed here. I think that should be a consideration in this.

Can you specify what behavior you think is right for those cases? undefined is not a single sanitizer, but a set of them (as is, e.g. bounds).

-fsanitize-trap=all -fsanitize=undefined
-fsanitize-trap=undefined -fsanitize=array-bounds
-fsanitize-trap=bounds -fsanitize=array-bounds
-fsanitize-trap=null,array-bounds -fsanitize=array-bounds

NAK. Copied my comment at [WIP][DO NOT MERGE][Clang][Driver] Emit warning when -fsanitize-trap=<...> is passed without associated -fsanitize=<...> by anthonyhatran · Pull Request #147997 · llvm/llvm-project · GitHub

Thank you for your efforts to enhance sanitizers. However, the current implementation aligns with the preferences of many, including GCC contributors who introduced -fsanitize-trap in 2022. Speaking in my capacity as a clang driver maintainer, sanitizer contributor, and GCC committer, gaining consensus to modify -fsanitize-trap to issue warnings or errors seems unlikely, so closing this issue appears to be the appropriate action.

The proposed change could serve an educational purpose by illustrating modifications to the driver and sanitizer components. However, pursuing its development within llvm/llvm-project risks causing confusion among contributors.

Objection from a previous discussion: [clang][GSoC 2025] Usability Improvements for trapping Undefined Behavior Sanitizer - #11 by jyknight


$ clang -c foo.cpp -la

clang: warning: -la: 'linker' input unused [-Wunused-command-line-argument]

The analogy doesn’t apply. Separate compilation and linker options, such as CFLAGS and LDFLAGS, are a well-established practice. Users are expected to avoid placing linker options in CFLAGS.

1 Like

The build system example is you’ve pointed out is how many of the existing compiler flags are intended to work I don’t think we should change that, especially since we’re aligned with at least one other major compiler on the precise interaction. This was also highlighted by a few maintainers in the earlier discussion and again here.

I kind of see an argument that the trap flag by itself could maybe trigger a -Wunused-command-line-argument, but I’d expect a fair amount of pain for consumers to adapt build rules to be the result. things like this seem trivial, but have a massive effect when they reach downstream consumers of the toolchain.

I noticed that you mentioned an opt in warning in the proposal, but didn’t explore it (only the opt-out variant). I could see that providing some value to detect accidental flag changes, but I think our community is a bit wary of off-by-default diagnostics. There’s also a question as to how much value they provide vs cost of maintaining. I don’t think I have a good answer for you here (or much of an opinion, really), but I know these are aspects our community considers a lot, and thus many of our key maintainers have strong opinions about. Did you consider that as an alternative? fleshing out your proposal with alternative design options and providing rationale for why you didn’t consider them viable would probably be helpful.

1 Like

I am also against this change.

Many users already rely on behavior when you enable sanitizers with detailed `-fsanitize=` flag and adjust with less specific modifiers, -trap, -recover, -merge e.g.:

-fsanitize=address,null,bool -fsanitize-trap=all

The value of the warning is not high enough to justify breaking change.

Can you specify what behavior you think is right for those cases?

Example 1

$ clang -fsanitize=undefined  -fsanitize-trap=all foo.c

clang: warning: -fsanitize-trap=memtag has no effect because the "memtag" sanitizer is disabled; consider passing "-fsanitize=memtag" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=nullability has no effect because the "nullability" sanitizer is disabled; consider passing "-fsanitize=nullability" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=shift has no effect because the "shift" sanitizer is disabled; consider passing "-fsanitize=shift" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=cfi has no effect because the "cfi" sanitizer is disabled; consider passing "-fsanitize=cfi" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=undefined-trap has no effect because the "undefined-trap" sanitizer is disabled; consider passing "-fsanitize=undefined-trap" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=implicit-integer-truncation has no effect because the "implicit-integer-truncation" sanitizer is disabled; consider passing "-fsanitize=implicit-integer-truncation" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=implicit-integer-arithmetic-value-change has no effect because the "implicit-integer-arithmetic-value-change" sanitizer is disabled; consider passing "-fsanitize=implicit-integer-arithmetic-value-change" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=implicit-integer-conversion has no effect because the "implicit-integer-conversion" sanitizer is disabled; consider passing "-fsanitize=implicit-integer-conversion" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=implicit-conversion has no effect because the "implicit-conversion" sanitizer is disabled; consider passing "-fsanitize=implicit-conversion" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=integer has no effect because the "integer" sanitizer is disabled; consider passing "-fsanitize=integer" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=bounds has no effect because the "bounds" sanitizer is disabled; consider passing "-fsanitize=bounds" to enable the sanitizer [-Wsanitize-trap-mismatch]
clang: warning: -fsanitize-trap=all has no effect because the "all" sanitizer is disabled; consider passing "-fsanitize=all" to enable the sanitizer [-Wsanitize-trap-mismatch]

The output of this one really isn’t ideal

  • There are too many warnings. These probably need to coalesced into a single warning
  • Saying -fsanitize-trap=shift (etc.) without those being passed on the command line explicitly is confusing.
  • The -fsanitize-trap=all has no effect diagnostic is actually wrong.

If we werere to fix these problems the output should probably be something like

clang: warning: `-fsanitize-trap=all` is not enabling trapping for the following sanitizers because they are currently disabled: memtag, nullability, shift, cfi, undefined-trap, implicit-integer-truncation, implicit-integer-arithmetic-value-change, implicit-integer-conversion, implicit-conversion, integer, bounds`

Example 2

$ clang -fsanitize=array-bounds -fsanitize-trap=undefined foo.c
clang: warning: -fsanitize-trap=undefined has no effect because the "undefined" sanitizer is disabled; consider passing "-fsanitize=undefined" to enable the sanitizer [-Wsanitize-trap-mismatch]

This warning doesn’t make sense because IIRC the array-bounds sanitizer is included in undefined group. The correct behavior here would be to emit no warning.

Example 3

@atran Can you post the output of that for your sketch patch along with an explanation of whether or not that’s a good warning and what you think the correct output would be?

Example 4

$ clang -fsanitize=array-bounds -fsanitize-trap=null,array-bounds foo.c
clang: warning: -fsanitize-trap=null has no effect because the "null" sanitizer is disabled; consider passing "-fsanitize=null" to enable the sanitizer [-Wsanitize-trap-mismatch]

I think this warning makes sense.

I kind of see an argument that the trap flag by itself could maybe trigger a -Wunused-command-line-argument , but I’d expect a fair amount of pain for consumers to adapt build rules to be the result. things like this seem trivial, but have a massive effect when they reach downstream consumers of the toolchain.

This was the primary motivation. The fact we don’t warn for something like

$ clang -fsanitize-trap=undefined test.c

seems like a problem and inconsistent with Clang generally warning about unused driver flags.

I’m unfortunately all too familiar with the pain new warnings can cause on downstream having worked on compiler qualification. However, that does not stop new warnings being implemented upstream. This is the cost you pay every time you upgrade the compiler. You get new warnings which might find problems in your code, but they also might make building existing code a pain.

I noticed that you mentioned an opt in warning in the proposal, but didn’t explore it (only the opt-out variant). I could see that providing some value to detect accidental flag changes, but I think our community is a bit wary of off-by-default diagnostics.

We considered the “opt-in” approach but didn’t think it was the right one because the warning we proposed is meant to help users who are not familiar with how the -fsanitize-trap= flag works. Users unfamiliar with the semantics of those flags are very unlikely to know to add -Wsomething-warning-name to their compiler invocation.

What is really missing from this proposal is why you think this is important to do (other than consistency, which comes at the price of consistency with GCC). We have various reasons why this causes problems, and how even defining the exact semantics is not easy; I don’t see much reason for implementing this. If it forces everyone to add Wsanitize-trap-mismatch to their build systems for no real reason, no one really gains anything from this.

That being said, the current output for my examples is definitely unacceptable. I don’t agree with your suggested output either. IMO the reasonable semantics is for none of them to issue any diagnostics. That semantics is “if any of the specified sanitizers are enabled”, which is easy-ish to explain and consistent. Otherwise you have weird effects such as replacing bounds with its definition of array-bounds,local-bounds suddenly changes the semantics.

Users unfamiliar with the semantics of those flags are very unlikely to know to add -Wsomething-warning-name to their compiler invocation.

Agree

Let me try to clarify because it’s clear we haven’t articulated the reason for wanting to change the behavior clearly enough. While the fact that Clang’s behavior around the -fsanitize-trap= flag is inconsistent with how it treats other unused driver flags that is not the primary motivation for the RFC. The primary motivation is that current design means it’s extremely easy to accidentally not have UBSan switched on at all, which is a problem.

For example. The very first time I tried to use trapping UBSan I tried to do.

$ clang -fsanitize-trap=undefined test.c -o test

And much to my surprise I discovered that no runtime checks were being added and that Clang failed to tell that I had made this mistake, because for this particular compiler invocation it’s pretty obvious what the intent was (switch on UBSan in trapping mode). Subsequently I learned that I needed to also pass -fsanitize=undefined but this really is not obvious. If I was the only person who ran into this then this wouldn’t of been a big deal and I would’ve moved on. However, we received other reports internally at Apple that engineers found this behavior confusing. This is why I thought it would be worth looking into this problem because initially it didn’t sound difficult to fix.

Unfortunately the current design being deliberate means at the time this design was chosen a trade-off was made. It was made convenient for build systems to selectively enable/disable particular UBSan checks for different targets in the same build system, but at the cost of making it difficult to warn the user that they had UBSan checks unintentionally disabled.

In this thread I haven’t seen collective agreement that the problem this RFC identifies actually is. If we can’t agree on that then there isn’t much hope in having this RFC progress.

Given that the UBSan maintainers are not onboard with the proposed changes myself, @Michael137 , and @atran will pause pursuing this.

I recognize that I’m late to the party and this comment is unlikely to influence anything; but I’ve done my share of driver hacking, and I do have an opinion. However, my opinion is not rooted on matching -fsanitize and -fsanitize-trap arguments, but whether the options are specified at all.

$ clang -fsanitize-trap=undefined test.c

It would be nice for this to warn, however the warning should simply say that -fsanitize was not specified. It should not specifically say that -fsanitize=undefined was not specified. The user (if it’s interactive) or build-system coder can figure that out easily.

$ clang -fsanitize=undefined -fsanitize-trap=undefined -fno-sanitize=all

This should not warn. -fsanitize was specified on the command line, although it was later overridden by -fno-sanitize. Being overridden (and having no net effect) is different from being missing.

$ clang -fsanitize=undefined -fsanitize-trap=all

This should not warn. The -fsanitize-trap=all is there to say “whatever sanitizers are enabled, they should trap.” I don’t want to be bothered to repeat a whole list of sanitizers for both options, that’s added complexity in the build system (or interactive typing) for no benefit. The intent of the command is obvious and the driver should just do it.