Skip to content

Commit 7a528f3

Browse files
committed
Merge branch 'master' into coverity-triage-policy
2 parents e4a5188 + 5806aa9 commit 7a528f3

2 files changed

Lines changed: 67 additions & 162 deletions

File tree

‎.github/workflows/openssf-scorecard.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,6 @@ jobs:
6969

7070
# Upload the results to GitHub's code scanning dashboard.
7171
- name: "Upload to code-scanning"
72-
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
72+
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
7373
with:
7474
sarif_file: results.sarif

‎coverity/README.md‎

Lines changed: 66 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1,82 @@
11
# Triaging Coverity Scan findings
22

3-
This is the guide for reviewing static-analysis results for `mkl_random`. It has
4-
three parts:
3+
Static analysis runs on [Coverity Scan](https://scan.coverity.com) via
4+
[`.github/workflows/coverity.yml`](../.github/workflows/coverity.yml) (weekly + on
5+
demand). Analysis runs on Black Duck's servers; triage is done in the Scan web UI.
56

6-
1. **[Policy](#policy)** — how to approach *any* scan: where findings come from,
7-
why triage keeps resetting, and the checklist to work through each new report.
8-
2. **[Known findings](#known-findings)** — a catalog of findings already
9-
reviewed, so they are not re-investigated from scratch every scan.
10-
3. **[Evaluated and declined](#evaluated-and-declined)** — approaches considered
11-
for cutting the noise, and why they were not adopted.
12-
13-
Static analysis runs on the free [Coverity Scan](https://scan.coverity.com)
14-
service via [`.github/workflows/coverity.yml`](../.github/workflows/coverity.yml)
15-
(weekly, plus `workflow_dispatch`). Analysis happens on Black Duck's servers;
16-
triage (Classification / Action / Comment) is done in the Coverity Scan web UI
17-
and is keyed by **CID**.
18-
19-
---
20-
21-
## Policy
22-
23-
### Where findings come from
24-
25-
Coverity analyzes two kinds of source:
26-
27-
- **Generated code** — `mklrand.cpp`, produced by Cython from `mklrand.pyx`.
28-
- **Hand-written code** — the C++ under `mkl_random/src/` and its headers.
29-
30-
Within the generated `mklrand.cpp`, two kinds of functions appear:
31-
32-
- **`__Pyx_*`, `__pyx_pw_*`, `__pyx_tp_*`, `__pyx_mdef_*`** — Cython runtime
33-
boilerplate and Python-level wrappers. Findings here are ~always false
34-
positives. **Not editable** — regenerated on every build.
35-
- **`__pyx_pf_*`** — the C translation of the *bodies* of our `.pyx` functions.
36-
A genuine logic bug in `mklrand.pyx` could in principle surface here, so these
37-
are **not** blanket-dismissed — but in practice every `__pyx_pf_*` finding to
38-
date has also been a false positive, because Coverity cannot see Python-level
39-
invariants (dtype sizes ≥ 0, fixed-length return tuples, etc.).
40-
41-
The vast majority of findings are boilerplate artifacts in generated code, driven
42-
by Cython's single-source-for-many-build-configs templating (macros that expand
43-
to constants, `#if`-selected version branches). They are not defects in code we
44-
maintain.
45-
46-
### Why triage resets, and the Cython pin
47-
48-
A Coverity CID is meant to survive small code edits, but a Cython *version* bump
49-
regenerates `mklrand.cpp` wholesale — renaming helper functions and reshuffling
50-
line structure — which churns the CIDs across the generated unit and silently
51-
drops the triage attached to them. The same boilerplate then reappears under new
52-
CID numbers. This is exactly what happened once already.
53-
54-
To keep triage durable, **Cython is pinned in `coverity.yml`**.
55-
The pin only takes effect because the build step runs with
56-
`--no-build-isolation`, so meson-python uses the pinned Cython from the
57-
environment rather than build-isolating and pulling the latest; if that flag is
58-
ever removed, the pin becomes a no-op. Production builds leave Cython unpinned in
59-
`pyproject.toml`, so this does not constrain shipped wheels or Python support.
60-
Bumping the pin is a deliberate act; expect to re-triage the boilerplate
61-
afterwards using the [known-findings catalog](#known-findings) below.
62-
63-
(The generated bytes also depend on the numpy/cpython `.pxd` files, so the pin is
64-
not an absolute guarantee — but those change far less often than Cython itself.)
65-
66-
(Per-function suppression that would let Cython float freely is a Coverity
67-
*Connect* feature, not available on the free Scan service — analysis runs on
68-
Black Duck's servers, so the only repo-side lever is which translation units are
69-
uploaded. See the [hard-exclude option](#optional-dropping-the-generated-unit)
70-
for the one path-based alternative.)
71-
72-
### Group generated code with a Project Component
73-
74-
Coverity Scan's **Project Settings → Components** lets you define a named
75-
component from a **regex matched against each defect's file path**. Defects are
76-
then bucketed under their component, so you can filter the generated-code noise
77-
out of view in one click while the hand-written code stays front-and-centre. This
78-
is path-based, so it survives Cython version bumps (unlike CID-keyed triage).
79-
80-
Recommended component to group (not hide) the generated unit:
81-
82-
- **Name:** `Cython-generated`
83-
- **Path regex:** `.*/mklrand\.cpython.*`
84-
85-
That pattern matches only the generated `mklrand.cpp` (the analyzed path looks
86-
like `/build/cp312/mklrand.cpython-312-...`); the hand-written sources live under
87-
`mkl_random/src/` — with an underscore, no `mklrand` substring — so they are not
88-
caught.
89-
90-
Two caveats:
91-
92-
- A component is **path-granular**, so it cannot separate the `__Pyx_*`
93-
boilerplate from the `__pyx_pf_*` bodies (both live in `mklrand.cpp`). That is
94-
fine for *grouping*; it is why the same page's option to mark a component
95-
*ignored* (dropping its defects from analysis entirely) is **not** recommended
96-
here — it would also drop the `__pyx_pf_*` bodies. Same trade-off as the
97-
[hard-exclude option](#optional-dropping-the-generated-unit).
98-
- Grouping complements the Cython pin; it does not replace it. The pin keeps
99-
triage from resetting; the component keeps the noise visually contained.
100-
101-
### Review checklist for each new scan
102-
103-
Do **not** blanket-ignore the generated unit — that could hide a genuine
104-
`.pyx`-logic bug. Instead, prioritise:
7+
Most findings are false positives in Cython-**generated** `mklrand.cpp`, not in
8+
code we maintain. This guide records why, and how to keep triage from resetting.
1059

106-
1. **Any finding in the hand-written code** under `mkl_random/src/`. This is the
107-
most likely place for a genuine defect — review every one.
108-
2. **Any High/Medium finding in a `__pyx_pf_*` function** (our translated logic).
109-
Verify against the `.pyx` source; if it reduces to a Python-level invariant
110-
Coverity can't see (dtype size, fixed tuple length, guarded index), mark it
111-
`False Positive` with a one-line reason.
112-
3. **Everything matching the boilerplate families below** — triage
113-
`False Positive` / `Ignore` in bulk, referencing this file.
10+
## Where findings come from
11411

115-
---
12+
- **Generated** `mklrand.cpp` (from `mklrand.pyx`): `__Pyx_*` / `__pyx_pw_*` /
13+
`__pyx_tp_*` helpers and wrappers are boilerplate — findings here are ~always
14+
false positives (see the table below). `__pyx_pf_*` functions are the C
15+
translation of our `.pyx` bodies; a real `.pyx` bug could surface here, though
16+
so far all have been false positives too (Coverity can't see Python-level
17+
invariants like non-negative dtype sizes or fixed-length tuples).
18+
- **Hand-written** C++ under `mkl_random/src/`. Most likely place for a real bug.
11619

117-
## Known findings
20+
## Keeping triage durable: the Cython pin
11821

119-
Match a new finding on its **checker + mechanism**, not its CID number — CIDs get
120-
reassigned when the Cython pin is bumped or when Black Duck upgrades the analysis
121-
engine. The example function names below are from the pinned Cython 3.3.0 output;
122-
Cython renames these helpers between versions (e.g. the vectorcall builder was
123-
`__Pyx_VectorcallBuilder_AddArg` before 3.3.0), so treat them as illustrative.
22+
A Cython *version* bump regenerates `mklrand.cpp` wholesale, which churns the
23+
Coverity CIDs and silently drops their triage — the same boilerplate then returns
24+
under new CIDs. So **Cython is pinned in `coverity.yml`** (not `pyproject.toml`,
25+
so shipped wheels are unaffected). The pin works only because the build runs with
26+
`--no-build-isolation`; bumping it means re-triaging the boilerplate.
12427

125-
### False-positive families in generated `mklrand.cpp` — triage `False Positive` / `Ignore`
28+
## Reducing the noise: a Project Component
12629

127-
| Family | Checker | Why it's a false positive |
128-
| --- | --- | --- |
129-
| `__pyx_tp_traverse_*`, `__Pyx_CyFunction_traverse` | DEADCODE | `__Pyx_call_type_traverse` expands to the constant-`0` macro on standard-CPython builds, so `if (e) return e;` is dead. Guard is live only in the Limited-API build variant. |
130-
| `__Pyx_PyCode_New`, `__Pyx_CallSlotAsVectorcallUnpackDict` | DEADCODE | `__Pyx_PyTuple_SET_ITEM` expands to the void `PyTuple_SET_ITEM` yielding constant `0`, so `if (... != 0)` is dead. Guard is live only in the Limited-API variant. |
131-
| `__Pyx_AddTraceback` | DEADCODE | `c_line` is fixed at `0` unless the optional `CYTHON_CLINE_IN_TRACEBACK` feature is enabled, making the `-c_line` branch dead by default. |
132-
| `__Pyx_ParseKeywordDict` | DEADCODE | In the CPython < 3.13 branch `found` is only ever 0/1, so `if (found < 0)` is dead; the guard serves the ≥ 3.13 `PyDict_GetItemRef` path. |
133-
| `__Pyx_PyLong_As_*` (npy_int16/uint8/int32/uint32/npy_bool/int/uint16/unsigned_int/npy_int8/irk_brng_t, …) | DEADCODE | Per-C-type integer-conversion helper templates; dead branches are compile-time-selected version/overflow guards. |
134-
| `__pyx_pf_*` bodies with temp cleanup (e.g. `choice`, `multivariate_normal`) | UNUSED_VALUE | `__pyx_t_N = 0;` nulls a temporary after its reference is transferred (`__Pyx_DECREF_SET` / assignment), guarding the error path against a double-DECREF. Dead only on the straight-line path. |
135-
| `__pyx_pw_*` keyword wrappers (`rand`, `randn`, …) | CHECKED_RETURN | `PyDict_Size` (via `__Pyx_NumKwargs_VARARGS`) is captured into `__pyx_kwds_len` and checked on the *next* line (`if (... < 0) __PYX_ERR(...)`). The statistical heuristic misfires because the check is one line from the call, not inline. |
136-
137-
There is also a **`sanity_check_for_cython`** DEADCODE finding, from meson's own
138-
compiler-probe translation unit rather than `mklrand.cpp` — same disposition
139-
(`False Positive` / `Ignore`), it just lives outside the generated file.
140-
141-
### Individually verified false positive
142-
143-
- **OUT_OF_BOUNDS in `_seed_impl`** (flagged High): the tuple unpack
144-
`brng_token, stream_id = _parse_brng_argument(brng)` (`mklrand.pyx:1571`)
145-
generates `PyTuple_GET_ITEM(sequence, 1)`, guarded by the generated
146-
`if (unlikely(size != 2)) { … __PYX_ERR(...) }` — so the index-1 read is only
147-
reached when `size == 2`. Independently, `_parse_brng_argument` always returns a
148-
fixed 2-tuple (`mklrand.pyx:1533`). Coverity does not tie the `ob_item[1]`
149-
access back to the guard across the macro. No out-of-bounds access is possible.
150-
151-
---
30+
**Project Settings → Components** buckets defects by a path regex. Define one to
31+
group (not hide) the generated file so it can be filtered out of view — path-based,
32+
so it survives Cython bumps:
15233

153-
## Evaluated and declined
34+
- **Name:** `Cython-generated` **Path regex:** `.*/mklrand\.cpython.*`
35+
36+
This matches only the generated file (hand-written sources are under
37+
`mkl_random/src/`, no `mklrand` substring). Group only — do **not** mark it
38+
*ignored*, as that also drops the `__pyx_pf_*` bodies (see [declined](#evaluated-and-declined)).
39+
40+
## Review checklist
15441

155-
### Modeling files
42+
Don't blanket-ignore the generated file — prioritise instead:
15643

157-
Coverity's modeling-file feature corrects the *behavior of called functions* it
158-
can't infer (custom allocators, panics, sanitizers). Almost all of our false
159-
positives are intraprocedural artifacts of generated code, `#if` branches, and
160-
macros — none of which a callee model can reach. The one partial exception is the
161-
`PyDict_Size` CHECKED_RETURN family, which a function model could influence — but
162-
that is a couple of findings against a CPython/Cython-internal callee, not worth
163-
the modeling maintenance. So modeling files are not adopted here.
44+
1. **Findings under `mkl_random/src/`** — review every one.
45+
2. **High/Medium findings in `__pyx_pf_*`** — verify against the `.pyx`; if it's a
46+
Python-level invariant Coverity can't see, mark `False Positive` with a reason.
47+
3. **Boilerplate families below** — bulk-triage `False Positive` / `Ignore`.
16448

165-
### Optional: dropping the generated unit
49+
## Known false-positive families
16650

167-
If the boilerplate ever outweighs its value, the generated translation unit can be
168-
removed from analysis *before upload* (repo-side, durable, path-based) by
169-
inserting this after the `cov-build` step in `coverity.yml`:
51+
Match on **checker + mechanism**, not CID (CIDs reset on a Cython bump or engine
52+
upgrade). Helper names below are from Cython 3.3.0 and vary between versions.
17053

171-
```bash
172-
cov-manage-emit --dir cov-int --tu-pattern "file('.*mklrand.*\\.cpp')" delete
173-
```
54+
| Family | Checker | Why it's a false positive |
55+
| --- | --- | --- |
56+
| `__pyx_tp_traverse_*`, `__Pyx_CyFunction_traverse` | DEADCODE | `__Pyx_call_type_traverse` is a constant-`0` macro on standard CPython, so `if (e) return e;` is dead; live only in the Limited-API build. |
57+
| `__Pyx_PyCode_New`, `__Pyx_CallSlotAsVectorcallUnpackDict` | DEADCODE | `__Pyx_PyTuple_SET_ITEM` expands to void `PyTuple_SET_ITEM` yielding `0`, so `if (... != 0)` is dead; live only in the Limited-API build. |
58+
| `__Pyx_AddTraceback` | DEADCODE | `c_line` is `0` unless the optional `CYTHON_CLINE_IN_TRACEBACK` feature is on, so the `-c_line` branch is dead by default. |
59+
| `__Pyx_ParseKeywordDict` | DEADCODE | In the CPython < 3.13 branch `found` is only 0/1, so `if (found < 0)` is dead; that guard serves the ≥ 3.13 `PyDict_GetItemRef` path. |
60+
| `__Pyx_PyLong_As_*` (per C type) | DEADCODE | Integer-conversion helper templates; dead branches are compile-time-selected version/overflow guards. |
61+
| `__pyx_pf_*` temp cleanup (e.g. `choice`, `multivariate_normal`) | UNUSED_VALUE | `__pyx_t_N = 0;` nulls a temporary after its ref is transferred, guarding the error path against a double-DECREF; dead only on the straight-line path. |
62+
| `__pyx_pw_*` keyword wrappers (`rand`, `randn`, …) | CHECKED_RETURN | `PyDict_Size` result is checked on the *next* line (`if (... < 0) __PYX_ERR(...)`); the statistical heuristic misfires because it's one line from the call. |
63+
64+
`sanity_check_for_cython` (DEADCODE) is the same disposition but comes from
65+
meson's compiler-probe unit, not `mklrand.cpp`.
66+
67+
**Verified individually — OUT_OF_BOUNDS in `_seed_impl` (High):** the unpack
68+
`brng_token, stream_id = _parse_brng_argument(brng)` (`mklrand.pyx:1571`) generates
69+
`PyTuple_GET_ITEM(sequence, 1)`, guarded by `if (unlikely(size != 2)) __PYX_ERR(...)`,
70+
and `_parse_brng_argument` always returns a fixed 2-tuple (`mklrand.pyx:1533`).
71+
Coverity just doesn't tie the index back to the guard. No overflow possible.
72+
73+
## Evaluated and declined
17474

175-
This is a "hard exclude" — it also drops the `__pyx_pf_*` bodies, trading away the
176-
(so-far theoretical) chance of catching a `.pyx`-logic bug for zero boilerplate
177-
noise. Left disabled by default in favour of the review checklist above.
75+
- **Modeling files** correct the behavior of *called* functions; our FPs are
76+
intraprocedural (dead branches, macros, `#if`), which models can't reach. The
77+
only fit is the `PyDict_Size` CHECKED_RETURN case — not worth the maintenance.
78+
- **Dropping the generated unit** (hard exclude), e.g. after `cov-build`:
79+
```bash
80+
cov-manage-emit --dir cov-int --tu-pattern "file('.*mklrand.*\\.cpp')" delete
81+
```
82+
Also drops the `__pyx_pf_*` bodies, so it's disabled in favour of the checklist.

0 commit comments

Comments
 (0)