Motivation
Compiler warnings can identify potential security, reliability, and correctness problems in C and C++ source code. However, it can be challenging for a project to “get clean” and “stay clean” on a particular warning. There may be hundreds of existing instances of that warning in the codebase, and it could take weeks or months to fix them all. Meanwhile, developers are also writing new code, potentially introducing new instances of that warning. And throughout this process, security experts may want to keep an eye on the changes, to make sure that developers are only suppressing instances of a warning when it it truly safe to do so.
Scenarios
Auditing
Before expending the effort to fix a particular set of compiler warnings in a project, it is important to understand the current state of the project with respect to those warnings. If the project already enables the relevant warnings, then this is simply a matter of building the project and inspecting the diagnostics emitted in the build log. However, many projects enable only those warnings that they expect their developers to address immediately, so the build log will not contain any warnings that the project is not yet interested in.
In order for the project owners to observe the state of the project with respect to warnings that are not yet enabled, we propose extending the compiler to generate an “audit log”. The project owners will specify the set of warnings they are interested in auditing, and the compiler will emit a separate file listing all instances of those warnings found by the compiler, even if those warnings were disabled or suppressed by one of the existing diagnostic control mechanisms (e.g., -Wno-<id>, #pragma clang diagnostic ignore, etc.). The auditors can then scan this audit log to estimate how much effort it would be to adopt a particular compiler warning on this project, and how vulnerable the project might currently be to the defects detected by those warnings. The auditors can combine this data with other information about the project in making this decision. For example, security auditors might look for projects that are known to have a large attack surface area and have a significant number of “uninitialized variable” warnings, and focus their adoption efforts on such projects while saving lower-risk projects for later.
By making the audit log a separate file, the project’s developers continue to see the same set of warnings that were seeing before auditing was enabled, so their day-to-day workflow is not disrupted.
Adoption
Once the decision has been made to adopt a particular new set of compiler warnings for a project, the audit log can feed into additional tooling to help the project’s developers track their progress on addressing the new warnings. The project would enable the new warnings in its build configuration, but then suppress existing instances of those warnings as a “baseline”. Any new instances of the warning would show up in the build as usual, making it easy to stop new issues from being introduced while the developers are trying to clean up the existing ones. Meanwhile, the developers can work through the backlog of baseline warnings over time, using the audit log to verify that those previously-suppressed warnings are now fixed.
Requirements
To build tooling to help with this process, we need information about the warnings beyond what Clang currently provides. In particular, we need to know:
- All of the warnings that are present in a particular build of a project, including those warnings that were suppressed via a
#pragma clang diagnostic ignored "...", a warning suppression mapping, or other such mechanism. This lets the project’s developers track which of the warning instances that they marked as “baseline” have been fixed, and which remain in the codebase. It also helps auditors track the overall state of those warnings in the project, and lets them review the decision to suppress particular warning instances, either at PR time, or at a later point. - For each warning that is suppressed, the location of the suppression itself. This makes it easier to tell how the warning was suppressed, especially when the suppression is located far away from the code where the warning was reported.
- The set of warnings that were checked for in each translation unit (TU), regardless of whether any instances of a particular warning were actually emitted for that TU. This lets us ensure that the right warnings are enabled in the first place. A compilation that produces no warnings about unsafe API usage has two very different meanings depending on whether that warning was enabled or not.
Clang has most of the above information available internally, or can readily compute it. We just need a way to emit it so that a warning management system can consume it later.
This RFC proposes emitting this information using the Static Analysis Results Interchange Format (SARIF). SARIF is a JSON-based standard for representing the results of static analysis tools, including information to support auditing of those results.
Usage
To generate a SARIF audit log, the user specifies the new --sarif-log option, plus zero or more instances of the new -Waudit=id option, where id is the name of a warning group (the same as for the existing -Wid option).
clang -c file.cpp -Waudit=uninitialized -Waudit=deprecated-declarations -Wuninitialized --sarif-log
This will instruct the compiler to generate a SARIF log file that contains all instances of the warnings specified by -Waudit=id, including instances whose diagnostic level was ignored. In addition, a SARIF rule object will be emitted for every warning specified by -Waudit=id, regardless of whether any instances of that warning were emitted.
Note that -Waudit=id does not actually change the initial level of the specified warning. Thus, in the above command line, only the uninitialized warnings are initially at level warning; the deprecated-declarations warnings are initially at level ignored, although even those ignored instances will be emitted to the audit log (with suppression information).
The default path for the audit log is the path of the object file with its extension replaced with .sarif. (e.g., file.sarif). This path can be changed by specifying the new --sarif-log=path option.
Proposed Changes
New command-line settings
--[no-]sarif-log
This switch enables (or disables) generation of the SARIF log file. Default: disabled.
--sarif-log=path
This switch specifies the path of the SARIF log file. Default: the path of the object file, with its extension changed to .sarif. Specifying this switch implicitly enables --sarif-log.
-W[no-]audit=id
This switch marks the warnings in the specified group as “audited”. Audited warnings are emitted to the SARIF log with additional information described in a later section. Default: not audited. This switch does not affect the level of the specified warnings (i.e., -Waudit=uninitialized does not imply -Wuninitialized).
By design, there is no mechanism for changing the “audited” state of a diagnostic from within the source code, because that would defeat the point of auditing. While the user is of course free to disable auditing for a warning on the command line (either by -Wno-audit=id, or by just never specifying -Waudit=id in the first place), the SARIF log records which warnings were audited vs. not audited. Thus, the consumer of the SARIF log can audit the lack of auditing of warnings.
See Why do we need a new switch to enable auditing for a warning? below for more on why this switch is needed.
Warning IDs and Names
The SARIF format requires a stable string identifier for each warning. Clang’s warnings do not currently have user-visible IDs. We will add support for specifying a stable ID for each warning in the .td file entry for that warning. All IDs will be prefixed with clang., to distinguish them from warning IDs from the Clang static analyzer.
For warnings that do not yet specify a stable ID, we will use the in-source name of the warning (e.g., clang.warn_uninit_var). Should someone later decide to change the in-source name of the warning, they would have to explicitly set the stable ID of that warning to the old name.
Note that SARIF does provide a way for a rule to declare a list of “deprecated IDs”, so once we do assign a stable ID to a warning, we could also emit its original in-source ID in its deprecatedIds property.
Also, note that SARIF specifically suggests that a rule’s id property is not intended to convey information about the rule to the user. Thus, the existing in-source names of warnings might well be acceptable as a SARIF rule ID, despite not being particularly friendly.
The SARIF format also allows a rule to specify a name property with a more human-readable name. We will allow a Clang diagnostic to specify this name in its .td definition. If not specified in the .td, no name property will be emitted for that diagnostic.
The SARIF Log File
The SARIF log file serves as a machine-readable representation of the usual diagnostics output. This serves essentially the same purpose as the serialized diagnostics log (controlled by the --serialize-diagnostics switch), except that the output can be consumed by IDEs, viewers, and results management tools that understand SARIF.
Audited vs. Non-Audited Warning Differences
The SARIF information generated for a particular warning depends on whether that warning is audited (controlled via -Waudit=id) or not. The differences are as follows:
runs[0].results[]
For a non-audited warning, a result object will be emitted for each instance of that warning, unless that warning had a diagnostic level of ignored at the point in the code where the warning was discovered. This is the same set of warning instances that would be emitted to the console output. Note that a warning that is off by default and never enabled on the command line or via #pragma clang diagnostic will always have a level of ignored, and thus will not be emitted.
For an audited warning, a result object will generated for every instance of that warning, even if the warning’s level was ignored.
runs[0].results[].suppressions[]
For a non-audited warning the suppressions property of each result will be an empty array, since the warning would not have been emitted if it had been suppressed.
For an audited warning, if the warning’s diagnostic level was ignored at the point in the code where the warning was discovered, the suppressions array will contain a single element describing what caused that warning’s level to be ignored (e.g., the #pragma clang diagnostic ignored directive, the warning suppression map file, etc.). If the warning’s level was not ignored, the suppressions array will be empty.
runs[0].tool.driver.rules[]
For a non-audited warning, a rule object will be emitted only if at least one result for that rule is present in the log.
For an audited warning, a rule object will be emitted regardless of whether any results were emitted for that rule.
runs[0].invocations[0].ruleConifigurationOverrides[]
For a non-audited warning, no configurationOverride object will be emitted for that rule.
For an audited warning, a configurationOverride object will be emitted for that rule, with its parameters property containing a property named clang/audited with the value true. SARIF does not appear to have a way to distinguish rules for which all suppressed results are emitted from rules for which only unsuppressed results are emitted, so we’ll record that distinction here. We should consider working with the SARIF committee to add such a capability to a future version of the SARIF specification.
Rationale / Alternatives
Why SARIF?
The SARIF format can represent all of the necessary information, and Clang already has some limited support for emitting SARIF. The Clang static analyzer (via --analyzer-output=sarif) can already emit a SARIF log containing the warnings reported by the analyzer, including rich information about the relevant control flow paths that lead to the warning. The Clang compiler itself (via -fdiagnostics-format=sarif) also has the ability to emit a SARIF log, although this support is still unstable. It makes sense to extend this support rather than inventing a new, non-standard format.
Why do we need a new switch to enable auditing for a warning?
The two main requirements for the SARIF log are that it contain all instances of each audited warning, including suppressed instances, and that it record which warnings were audited, even if there are no instances of a particular warning. Without a way to explicitly specify which warnings are audited, we would have to determine the set of audited warnings another way. The following options were considered; we can revisit them if there’s a strong aversion to the -Waudit=id switch.
Option 1: All warnings are audited
If we decided to just enable auditing for all Clang warnings, we would be able to generate a SARIF log that meets the two main requirements. However, that log would be unacceptably large.
We expect auditing and policy enforcement to care about a relatively small number of specific warnings (e.g., security-relevant warnings). If we made all warnings audited, though, then the SARIF log would contain a (suppressed) result for every instance of every warning in the source file and all of its included headers. This would include pedantic warnings that nobody cares about, portability warnings that are irrelevant for a single-platform codebase, etc. There would likely be more of these uninteresting results than interesting results, at least doubling the size of the log without benefit.
Even if the number of uninteresting warning instances were manageable, we would still be emitting rule metadata for every Clang warning, regardless of whether it actually generated any warning instances. The metadata for these hundreds of diagnostics would take up a large amount of space on its own, again with no benefit.
Option 2: Only warnings that are enabled on the command line are audited
So why not just enable auditing for the warnings that are enabled on the command line via -Wid? The first problem here is that all of the existing diagnostics command-line options control the initial level of each warning, but the source code can change that level in either direction via #pragma clang diagnostic. If a warning is initially ignored, but later elevated to warning via a #pragma, do we consider it as audited? If so, we need to record every suppressed instance of every warning, so that we can emit those suppressed results if the warning does get elevated later on.
Note: The latest version of the Clang User’s Manual claims that “upgrading” a warning that was initially disabled on the command line is not possible “yet”, but empirically, this seems to be supported in current versions of Clang.
The second problem is that this approach provides no distinction between “not audited” and “audited but suppressed for this translation unit”. When trying to clean up all instances of a particular warning across a codebase, it is common to fix warnings file-by-file or subproject-by-subproject. When the developer is ready to fix the warnings in a particular file, they enable that warning just for that file. They then fix whatever warnings are reported in that file before moving on to the next file. The warning remains disabled in all of the files that the developer has not reached yet. We want the warnings from the not-yet-reached files to show up as suppressed results in the SARIF log, so that the develop can track what warning instances remain to be fixed.
Related Work
[RFC] Add a new text diagnostics format that supports nested diagnostics
The linked RFC makes some great improvements to the human-readable diagnostic output, but those should be orthogonal to the changes proposed here, which focus on machine-readable output.
[RFC] Hardening mode for the compiler
Part of the linked RFC proposes that hardening mode could enable additional warnings to go along with any other language and code generation changes. This is certainly helpful for projects trying to adopt more secure coding practices, which is the scenario targeted by the changes proposed here as well, but for now, the two RFCs appear orthogonal.
Future Work
Tracking Warnings over Time
One important capability for a warning management system is tracking warnings over time: spotting when new warnings get introduced, or when known warnings get fixed. This requires some way to tell whether two warnings, reported against different commits of the same repo, are “the same” warning. There are a few potential approaches to this problem:
- Add a unique ID to each suppressed warning in source code, via a comment or attribute. Advantages: moves with the code; visible to developers working on the code. Disadvantage: Unsightly, especially if there are a lot of suppressions, such as when a codebase first starts to adopt a particular warning and has to suppress all existing instances of that warning as a “baseline”.
- Compute a “fingerprint” of the relevant code. Advantages: Can be used without modifying source code, so it works well for “baselining” scenarios; we have such an algorithm in the Clang static analyzer already. Disadvantages: Less visible to developers; no fingerprinting algorithm is resilient to exactly the right set of changes.
- Tracking lines through Git history. Advantage: Can be used without modifying source code, so it works well for “baselining” scenarios. Disadvantages: Less visible to developers, requires access to Git commit history, still a bit of a research project.
Without more experience with how any of the above approaches work in practice, we’re not yet ready to propose a particular approach to integrate into Clang. We’d like to solicit other ideas from the community, and will share the results of our own experiments with the community.
Unification with Clang Static Analyzer
We would like to have the Clang Static Analyzer and the regular Clang compiler emit their warnings to the same SARIF log, with the same support for auditing. While the implementation of this RFC will attempt to reuse code between the compiler and analyzer where practical, unifying the two different sets of command line options, as well as the different suppression mechanisms, will be deferred to a future RFC.