|
1 | 1 | # Triaging Coverity Scan findings |
2 | 2 |
|
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. |
5 | 6 |
|
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. |
105 | 9 |
|
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 |
114 | 11 |
|
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. |
116 | 19 |
|
117 | | -## Known findings |
| 20 | +## Keeping triage durable: the Cython pin |
118 | 21 |
|
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. |
124 | 27 |
|
125 | | -### False-positive families in generated `mklrand.cpp` — triage `False Positive` / `Ignore` |
| 28 | +## Reducing the noise: a Project Component |
126 | 29 |
|
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: |
152 | 33 |
|
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 |
154 | 41 |
|
155 | | -### Modeling files |
| 42 | +Don't blanket-ignore the generated file — prioritise instead: |
156 | 43 |
|
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`. |
164 | 48 |
|
165 | | -### Optional: dropping the generated unit |
| 49 | +## Known false-positive families |
166 | 50 |
|
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. |
170 | 53 |
|
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 |
174 | 74 |
|
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