Skip to content

[doc] HLD: Static analysis CI gates for SONiC repositories - #2503

Open
bhouse-nexthop wants to merge 11 commits into
sonic-net:masterfrom
bhouse-nexthop:hld/static-analysis-ci
Open

[doc] HLD: Static analysis CI gates for SONiC repositories#2503
bhouse-nexthop wants to merge 11 commits into
sonic-net:masterfrom
bhouse-nexthop:hld/static-analysis-ci

Conversation

@bhouse-nexthop

@bhouse-nexthop bhouse-nexthop commented Aug 17, 2026

Copy link
Copy Markdown

Pull Request Template

What is being proposed

A high-level design for a static analysis merge gate across SONiC's own repositories, covering Python, C/C++, Rust, Go and shell.

The analysis runs inside the Azure pipelines each repository already has — extra steps in the existing build job for compiled languages, and a small standalone stage for Python and shell, which need no build. No new CI system, no new build agents.

Rendered: doc/security/static-analysis-ci-hld.md

This is a design document only. No code, pipeline, or build change is included.

Why

SONiC has no working static analysis gate. The only lint check in the tree, flake8 in sonic-utilities, is declared continueOnError: true and cannot block a merge. The CodeQL and Semgrep workflows report but do not gate, and the evidence that nobody acts on them is unambiguous:

  • sonic-buildimage has over 3,100 open CodeQL alerts. Of the first 3,000 sampled, 50 carry a security tag and 2,950 do not. CodeQL is also configured language: [ 'python' ] in every repository that has it, including sonic-swss and sonic-sairedis — so it analyses none of their ~1,150 C/C++ files.
  • The Semgrep workflow has failed 100 of its last 100 pushes to master, reporting 338 blocking findings. Pull requests pass only because semgrep ci diff-scopes on PR events.

The document is careful to separate two claims that are easy to conflate, and §4.2 says so explicitly:

The security argument rests on C/C++, and is not yet measured. Memory safety across ~1,700 files in sonic-swss, sonic-sairedis, sonic-swss-common, linkmgrd and others — none of which has ever been run through a static analyzer — is what a security-motivated gate is actually for. Producing those numbers needs the full build environment, which is what the pilot provides. That measurement is a pilot exit criterion, not a claim made up front.

What has been measured is correctness, not security. 445 latent NameError sites (raw_input, unicode, an unimported syslog) clustered in platform error paths; 56 files still written in Python 2 that raise SyntaxError on import and cannot have worked since 202012; and on the Go side, nil dereferences and unchecked json.Unmarshal/io.Copy/os.Remove. Real defects, but crashes and logic errors rather than vulnerabilities.

How it works

A shared sonic-ci repository holds the analyzer configuration and one manifest deciding what blocks a merge. Consuming repositories carry ~10 lines, pinned by tag, and never edit them again to change rules or tool versions.

Rules fall into three scopes:

  • gate — blocks anywhere in the repository. A small, high-confidence set whose backlog is cleared during rollout.
  • gate-new — blocks only defects a pull request introduces. This is what makes a 35,862-finding backlog survivable without a cleanup nobody would do.
  • advisory — reported, never blocks.

"Introduced" is decided by comparing finding sets against a baseline the target branch publishes, not by filtering on changed lines. §7.4.1 shows why with a worked case: removing a null check reports the dereference several lines away, on a line the PR never touched, so a line filter waves a real null dereference through. Findings are matched by content fingerprint so they survive line shifts and re-indentation; the baseline is produced on every merge and consumed via the same artifact mechanism pipelines already use for libswsscommon and sairedis, so a PR still costs one analysis pass rather than two.

Design decisions backed by measurement

  • clang-tidy rather than scan-build — 119 of its checks are the Clang Static Analyzer, verified to reproduce the identical path-sensitive diagnostics, plus ~419 pattern checks scan-build has no equivalent for. Same cost, strict superset.
  • The Go linters are tuned, not enabled wholesale. Running the obvious set over the 27 sonic-gnmi packages that build without cgo produced 122 findings, of which errcheck and revive were 100 — 35 of those merely missing doc comments, 29 unchecked Close/Flush on cleanup paths. Disabling revive's documentation rules and excluding the conventional errcheck functions yields ~43 actionable findings while losing no staticcheck, govet, unused or ineffassign result.
  • Existing tooling is accounted for, not silently replaced (§7.5.6). sonic-utilities' flake8 line-length and ignore set carry across to ruff; sonic-mgmt-framework's pylint keeps running because it is a build target; sonic-dash-ha's pre-commit config — file hygiene, detect-private-key, actionlint, cargo fmt — is generalised into the shared set rather than left in one repository.

Developer experience

Everything runs locally through the same entry point CI uses, reading the same manifest, with the pinned tool versions bootstrapped automatically. C/C++ and Go need a build, and the document says so plainly rather than pretending otherwise, giving both the local and container paths. Per-repository instructions land with each adoption PR — a repository is not considered adopted until its README section exists.

Scope and rollout

master only; release branches are not backported to. Vendor-hosted platform submodules and vendored upstream projects (FRR, scapy, ptf) are excluded — the community cannot merge changes there.

Five phases: foundation; a four-repository pilot (sonic-utilities, sonic-gnmi, sonic-swss, dhcpmon), all of which already run trixie so none waits on a migration; retiring the bookworm jobs, ordered by which artifacts other repositories still fetch; fleet rollout, gated on the pilot's cost measurements being within budget; Python 2 removal; then rule promotion.

Review notes

The sections most worth scrutiny:

  • §7.4 — why baseline comparison rather than changed-line filtering, and how findings are matched across revisions. This is the least obvious part of the design.
  • §7.5.4 — the Go tuning, since the specific rules disabled are a judgement call.
  • Appendix B — deleting the 56 Python 2 files. One PR per vendor, same route SONiC already uses to retire platform support.

Also adds doc/security/README.md indexing the directory and cross-referencing the security HLDs already elsewhere in doc/. Those are not moved.

Adds a high-level design for a static analysis merge gate covering Python,
C/C++, Rust, Go and shell across SONiC's own repositories, wired into the
Azure pipelines each repository already runs rather than into new CI jobs.

SONiC has no working static analysis gate today. The one lint check that
exists (flake8 in sonic-utilities) is declared continueOnError, and the
CodeQL and Semgrep workflows report without gating: sonic-buildimage has
over 3,100 open CodeQL alerts, 98% of them code quality rather than
security, and the Semgrep workflow has failed 100 of its last 100 pushes
to master with 338 blocking findings nobody acts on.

Measured against the current tree, that has left real defects in place --
445 undefined-name findings that are latent NameError crashes in platform
error paths, and 56 files still written in Python 2 that raise SyntaxError
on import under any supported image.

The design proposes a shared sonic-ci repository holding the analyzer
configuration and one manifest deciding which rules block a merge, so each
repository carries about ten lines. Rules either block anywhere in the
repository, or block only defects a pull request introduces -- measured by
comparing findings against the target branch rather than by filtering on
changed lines, which testing showed misses regressions whose symptom lands
on an untouched line.

Also adds doc/security/README.md indexing this directory and pointing at
the existing security-related HLDs elsewhere in doc/.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 17, 2026

Copy link
Copy Markdown

CLA Missing ID

One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via:

Co-authored-by: name <email>

Supported Co-authored-by: formats include:

  1. Anything <id+login@users.noreply.github.com> - it will locate your GitHub user by id part.
  2. Anything <login@users.noreply.github.com> - it will locate your GitHub user by login part.
  3. Anything <public-email> - it will locate your GitHub user by public-email part. Note that this email must be made public on Github.
  4. Anything <other-email> - it will locate your GitHub user by other-email part but only if that email was used before for any other CLA as a main commit author.
  5. login <any-valid-email> - it will locate your GitHub user by login part, note that login part must be at least 3 characters long.

Alternatively, if the co-author should not be included, remove the Co-authored-by: line from the commit message.

Please update your commit message(s) by doing git commit --amend and then git push [--force] and then request re-running CLA check via commenting on this pull request:

/easycla

@azure-pipelines

Copy link
Copy Markdown
Commenter does not have sufficient privileges for PR 2503 in repo sonic-net/SONiC

The design described how a pull request consumes the target-branch
baseline but not how that baseline comes to exist, which left the
failure modes unspecified.

Adds: baseline mode runs on every merge to the target branch with all
rules enabled and nothing gating; pull requests download the most recent
completed artifact, so a PR still costs one analysis pass rather than
two. A weekly scheduled run repairs the cases a merge-triggered baseline
misses -- a branch with no recent merges, a build that failed before
publishing, and analyzer behaviour that changes without the code
changing. sonic-swss, sonic-sairedis and sonic-swss-common already carry
a suitable weekly cron; sonic-utilities needs one added.

Also specifies what happens when the baseline is missing, stale, or was
produced under different analyzer versions or a different rule set. In
each case the regression-only rules degrade to advisory and say why,
while repo-wide gating rules are evaluated from the pull request's own
findings and are unaffected -- so an unusable baseline can never let a
gating defect through, and can never block a pull request spuriously.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

Two gaps.

Fingerprinting was asserted rather than specified. Now gives the exact
key -- path, rule, hash of the stripped source line, and an occurrence
counter for repeats -- states that it must be computed during analysis
rather than reconstructed from stored line numbers, and covers the cases
that were previously unaddressed: multi-line findings, findings with no
source line, and the same header analysed through several source files.
Behaviour is tabulated against measured results: 20 lines inserted above
four findings reports 0 new, re-indenting reports 0, deleting one of four
identical findings reports 0, and a genuine new defect reports 1.

Second, the design replaced tools SONiC already runs without saying what
that changed. Adds 7.4.6 accounting for all three: sonic-utilities'
flake8 (line-length 120 and its default ignore set are carried across;
W503/W504 are dropped as no-ops since ruff does not implement them; C90
was never active), sonic-mgmt-framework's pylint (unchanged -- it is a
build target -- with ten of its eleven checks mapped to ruff equivalents
and W0621 noted as having none), and sonic-dash-ha's pre-commit config.

That last one exposed a real gap: it covers file hygiene, config-file
validity, private-key detection, actionlint and cargo fmt, none of which
this design had. Those are cheap and worth having everywhere, so they
become two shared entries in the manifest rather than staying in one
repository.

Also notes that the existing flake8 hook diffs HEAD^..HEAD, so on a
multi-commit pull request it only ever lints the final commit. Baseline
comparison covers the whole pull request.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

Go linters are noisy by default, and measurement confirms it. Running
govet, staticcheck, errcheck, ineffassign, unused and revive over the 27
sonic-gnmi packages that build without cgo produced 122 findings, of
which errcheck and revive accounted for 100. Most of that is not defect:
35 of revive's 50 are missing doc comments, and 29 of errcheck's 50 are
unchecked Close/Flush/Sync on cleanup paths.

Enabling that set as-is would bury 22 useful findings -- including
SA5011 nil dereferences and unchecked json.Unmarshal, io.Copy and
os.Remove -- under a hundred nobody reads, which is how linters end up
switched off. The configuration now disables revive's documentation
rules and excludes the conventional errcheck functions, taking 122 raw
findings to roughly 43 actionable ones without losing a single
staticcheck, govet, unused or ineffassign result.

Separately, section 4.2 presented Python lint findings as the evidence
for a document filed under doc/security/. Those findings are real, but
they are crashes and logic errors, not vulnerabilities, and ruff would
not catch a security issue in the first place. The section now separates
the two: the security argument rests on C/C++ memory safety across
~1,700 files that have never been analysed, and is explicitly stated as
not yet measured because it needs the build environment the pilot
provides. What has been measured is labelled as correctness, and the Go
sample is included so the evidence is not Python-only.

Also drops the downstream-fork requirement and its test case.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

bhouse-nexthop and others added 2 commits August 17, 2026 16:39
…agram

Local reproduction was referenced in five places -- a requirement, a
filename, two example commands and a test -- but never designed, so the
hard parts were unanswered: how a developer gets analyzers that live in
the slave image, how C/C++ analysis works without a compilation
database, and how anyone compares against a baseline that exists only as
a pipeline artifact.

Section 7.9 answers those. sonic-analyze bootstraps the pinned tool
versions locally, so a developer cannot accidentally run a different
ruff than CI does. Python, shell, Rust and the hygiene checks need no
build. C/C++ and Go do, and the document says so plainly, giving both
the local path (bear + make) and the container path rather than
pretending a compiled language can be linted without compiling. A
--since flag reconstructs a baseline from the merge base for developers
who want to see specifically what their branch introduces, opt-in
because it costs a second pass.

Per-repository documentation is now part of adoption rather than a
follow-up: the right commands differ by repository, so each Phase 2 PR
adds a local-usage section to that repository's README, and a repository
is not considered adopted until it exists. Adds R6a and ST10/ST11.

Also replaces the architecture diagram. The old one showed one
repository being referenced by three others, which the surrounding
sentence already said. The new one shows the mechanism that is actually
non-obvious: master publishing a baseline, a pull request downloading
it, and the three rule scopes resolving against the comparison.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The argument for comparing finding sets rather than filtering on changed
lines was a bold lead buried inside 7.3.1, so it did not appear in the
table of contents and was easy to miss. It is the one part of this
design that is genuinely counter-intuitive -- filtering on the diff is
the obvious implementation and it silently misses regressions -- so it
needs to be findable.

Splits that material into four numbered sections: gate scope, why
baseline differencing, fingerprinting, and producing and consuming the
baseline. All four now appear in the table of contents. 7.3.1 closes
with a pointer into the three that follow.

Also folds away the "Known behaviours" list, which had become mostly a
restatement of the fingerprinting and baseline sections written above
it. The two items that were not duplicated -- baseline drift and the
reporting of fixed findings -- are kept.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

…lot repos

Reordering the trixie migration to follow the pilot means sonic-swss can
no longer be a pilot repository, since it is one of the pipelines being
migrated. Choosing replacements surfaced a constraint worth recording.

Surveying which slave image each repository actually uses shows three
groups, not two. Six repositories already run trixie and can adopt
immediately. Four are parameterised and default to bookworm while
already running trixie jobs alongside. Ten pin sonic-slave-bookworm
directly and need a real port -- a larger set than the five previously
identified, because repositories with a hardcoded image do not carry a
debian_version parameter to flip.

The pilot is now sonic-utilities, sonic-gnmi and dhcpmon, all of which
run on trixie today. sonic-utilities is unaffected either way: its
analysis runs in a standalone stage with no container.

Two gaps follow from the reordering and are stated rather than papered
over. Rust cannot be piloted at all, because every Rust repository is
awaiting migration. C/C++ can only be piloted at dhcpmon's 9-file scale,
against sonic-swss's 564, so the wall-clock and peak-RSS figures R11
depends on are not produced by the pilot.

Both close in a new Phase 3, where sonic-swss adopts C/C++ and Rust
together and produces those measurements. Fleet rollout is gated on
Phase 3 rather than on Phase 1, so nothing rolls out fleet-wide on
unmeasured cost. Python 2 removal and rule promotion renumber to Phases
5 and 6.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

The three sections covering how a pull request's findings are told apart
from pre-existing ones were subsections of the severity manifest, which
made them look like details of rule configuration. They are one
inter-related argument and belong at the same level as the manifest
itself.

Adds 7.4 "Deciding what a pull request introduced" as a peer of 7.3,
holding what were 7.3.2 through 7.3.4: why baseline differencing beats
changed-line filtering, how findings are matched across revisions, and
where the comparison point comes from. Its introduction states why the
three are read together. 7.3.1 keeps the scope definition, since that is
part of the manifest, and now carries a one-line pointer instead of the
paragraph that duplicated the new introduction.

Everything from the old 7.4 onward shifts by one: per-language design is
now 7.5, suppressions 7.6, build dependency 7.7, rollout 7.8, items not
changed 7.9, and local use 7.10. All cross-references and the table of
contents are updated.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

Investigating the concern behind deferring the trixie work shows it
applies to sonic-swss-common but not to sonic-swss, and that the design
had conflated three different actions.

Attaching the gate to an existing trixie job is blocked by nothing.
Flipping the debian_version default is blocked by nothing as long as the
bookworm job still publishes. Only retiring the bookworm jobs is
disruptive, and only for repositories whose bookworm artifact something
else still fetches. sonic-swss-common-bookworm is fetched by four
repositories -- sonic-dash-ha, sonic-bmp, sonic-utilities and linkmgrd
-- so its retirement waits on them. sonic-swss-bookworm has no external
consumers at all.

The docker layer coupling is real but lives inside sonic-buildimage:
docker-swss-layer-bookworm.mk and docker-swss-layer-trixie.mk both
depend on the same $(SWSS), and which is built is selected by BLDENV
from the slave container buildimage itself runs in. sonic-buildimage
also builds swss from submodule source rather than from its published
artifact, so no submodule pipeline setting reaches it.

sonic-swss therefore returns to the pilot, attaching to the BuildTrixie
stage it already runs on every pull request. That restores C/C++ at
564-file scale and Rust coverage, so the wall-clock and peak-RSS figures
R11 depends on come from the pilot again rather than from a later phase.
dhcpmon stays as a second, smaller C/C++ case.

Retiring the bookworm jobs becomes Phase 2, ordered by who still fetches
what. Fleet, Python 2 removal and promotion renumber to Phases 3, 4 and
5. Notes that BuildTrixie is chained behind Build and BuildArm in both
repositories, so a gate attached there reports late and is skipped if an
earlier stage fails.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

The case is that these files cannot run on any supported image, so
deleting them loses nothing. That did not need a paragraph establishing
they are non-functional, a second one citing the cavium removal as
precedent and arguing SONiC has no deprecation process to follow, and
four bullets prescribing what each PR description must contain.

Cuts Phase 4 from roughly thirty lines to eight, keeping the vendor
grouping and the backlog figure, and trims the same reasoning where it
was restated in Appendix B and Section 10.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

The design had gate-new fall back to advisory when the baseline was
missing or unusable, and claimed as a safety property that the gate
never depends on the baseline being available. That was wrong in two
ways.

It is inconsistent with how SONiC pipelines already behave. The
DownloadPipelineArtifact tasks that fetch libswsscommon, sairedis and
libnl carry no failure tolerance, and allowPartiallySucceededBuilds only
relaxes which build to source from, not whether the artifact must exist.
A missing build asset already fails the job everywhere else.

More importantly it was a hole. A broken baseline job would quietly
switch off a whole class of enforcement with nothing failing to indicate
it, which is the failure mode this design exists to remove.

The baseline is now treated as a build dependency like any other: absent
or incomparable means the job fails. The two situations that could
produce that are handled by sequencing instead. A repository adopts in
two steps -- repo-wide gate rules first, which need no baseline, then
gate-new once the branch build has published one -- so the "no baseline
yet" case does not arise in steady state. A version or rule-set mismatch
fails and reports that the target branch must rebuild, a window of one
build cycle after a sonic-ci tag bump.

Updates UT13 and UT14, and the pilot and fleet criteria to require both
adoption steps.

Signed-off-by: Brad House <bhouse@nexthop.ai>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

nh-grecs Bot pushed a commit to nexthop-ai/SONiC that referenced this pull request Aug 23, 2026
rebuild-source: sonic-net#2503 @ bhouse-nexthop/SONiC 4f61909 [case: upstream:open]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants