codeql: fix argv-unchecked-index scope and its same-line guard - #2430
Merged
Conversation
Code-scanning alerts 2366 and 2367 (iccdev/argv-unchecked-index) both point at iccApplyProfiles.cpp:279, which is if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) -- a read of argv[1] that happens only because argc == 2 already held. Two alerts rather than one because the line reads argv[1] twice; they are not duplicates of each other, they are columns 29 and 55. Two defects in the query, which have to be fixed together: 1. hasArgcGuard() required the argc comparison on a strictly earlier line than the access. In the short-circuit help idiom the guard and the read share a line, so the guard was invisible and the read was reported. Same line is not on its own enough to call something guarded, though. Relaxing to a plain `<=` also swallows what the ordering test is for: in `if (!strcmp(argv[1], "-x") && argc > 2)` the read happens before argc is compared and is out of bounds at argc == 1. On a shared line this now compares columns, so the comparison has to start left of the access. That is still an approximation -- a one-line guard inverted relative to its access reads left to right and is missed -- and GuardCondition.controls() is the sound replacement for the whole predicate, which is far more than these alerts need. 2. isArgv() required the parameter's unspecified type to be a PointerType. A parameter written `char* argv[]` keeps its declared type in the extractor, so it is an ArrayType and never matched -- even though the predicate's own comment has always said "char** or char*[]". Measured against a database of the tree, the query was examining 13 of the 229 argv[N>0] reads under Tools/, i.e. 5.7% of its stated scope, and the two alerts above were the only thing it had to say about them. Fixing (2) alone would have made things worse: four of the five iccApply* tools use the array spelling *and* the same-line help guard, so widening isArgv() without relaxing the line test takes the tree from 2 false positives to 10. The test's arrayHelpGuardSameLine case pins that. Measured before and after on a database built from this tree: query scope 13/229 reads -> 229/229 reads findings 2 (both FP) -> 0 The 0 is a real result, not a silent one: all 229 reads now in scope carry an argc comparison ordered before them. hasArgcGuard() stays an ordering approximation rather than control-flow dominance, so that means "an argc comparison precedes this read", not "this read is provably guarded". Adds the red/green test the fix was developed against. It discriminates three ways: it fails on the unfixed query (the two same-line false positives present, the array-spelled true positives missing) and it also fails on a plain `<=` relaxation, which drops readBeforeGuardSameLine. Without that last case the suite could not tell a correct same-line rule from one that merely stops looking at the line. The test cases live in Tools/CmdLine/case.h rather than beside the .qlref because the query filters on a "Tools/" path prefix and the qltest cpp extractor does not descend into subdirectories: a case compiled at the test root is out of scope and the test passes having asserted nothing. That is not hypothetical -- the fixture did exactly that twice while it was being written, once reporting "found nothing to extract" and still PASSING. Nothing ran these tests. ci-codeql-security.yml builds a database and is gated behind the codeql-ready label, so the existing unbounded-profile-loop test had never executed in CI either. ci-codeql-query-tests.yml runs them on any PR touching .github/codeql-queries/**; it needs no database and no build. It asserts every test directory has a source file beside its .qlref first, so the extract-nothing failure cannot pass silently again, and it always installs the SHA-pinned bundle rather than trusting a codeql on the runner's PATH, because .expected asserts extractor locations exactly. Also gitignores *.testproj/, the databases codeql test run extracts beside each test directory, which were never ignored. docs/codeql.md named two CodeQL bundle pin sites; this adds a third, so the doc, and the new workflow's own header, now name all three.
colourbill-ctrl
requested review from
ChrisCoxArt,
dwtza,
maxderhak and
xsscx
as code owners
September 6, 2026 08:13
xsscx
pushed a commit
that referenced
this pull request
Sep 6, 2026
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two open
iccdev/argv-unchecked-indexcode-scanning alerts oniccApplyProfiles.cpp:279, and fixes the reason the query produced them.The alerts
Line 279 is the help-screen guard:
argv[1]is read only becauseargc == 2already held. Two alerts ratherthan one because the line reads
argv[1]twice — they are not duplicates ofeach other, they are columns 29 and 55.
Two defects, which have to be fixed together
1.
hasArgcGuard()required the guard on a strictly earlier line.cmp.getStartLine() < access.getStartLine()cannot see a guard thatshort-circuits on the same line, which is how every
Tools/CmdLinetoolspells its help screen.
Same line is not on its own enough, though. A plain
<=also swallows thecase the ordering test exists for —
if (!strcmp(argv[1], "-x") && argc > 2),where
argv[1]is read beforeargcis compared and is out of bounds atargc == 1. On a shared line the fix therefore compares columns: thecomparison has to start left of the access.
helpGuard(must not fire)readBeforeGuard(must fire)<(today)<=Still an approximation: a one-line guard inverted relative to its access
(
if (argc > 5) { … } else { … argv[3] … }) reads left to right and ismissed.
GuardCondition.controls()is the sound replacement for the wholepredicate and is much more than these alerts need.
2.
isArgv()required the parameter's unspecified type to be aPointerType. A parameter writtenchar* argv[]keeps its declared type inthe extractor, so it is an
ArrayTypeand never matched — even though thepredicate's own comment has always said "char** or char*[]".
Fixing (2) alone makes things worse: four of the five
iccApply*tools usethe array spelling and the same-line guard, so widening
isArgv()withoutrelaxing the line test takes the tree from 2 false positives to 10. The
arrayHelpGuardSameLinecase pins that.Measured, on a database built from this tree
argv[N>0]reads underTools/in query scopeThe
0is a real result, not a silent one: every one of the 229 reads now inscope carries an
argccomparison ordered before it.hasArgcGuard()remainsan ordering approximation rather than control-flow dominance, so that means
"an
argccomparison precedes this read", not "this read is provably guarded".Tests
Adds the red/green test the fix was developed against. It discriminates three
ways — it fails on the unfixed query (the two same-line false positives
present, the array-spelled true positives missing) and on a plain
<=relaxation, which drops
readBeforeGuardSameLine. Without that last case thesuite could not tell a correct same-line rule from one that merely stops
looking at the line.
The cases live in
Tools/CmdLine/case.hrather than beside the.qlrefbecause the query filters on a
Tools/path prefix and the qltest cppextractor does not descend into subdirectories: a case compiled at the test
root is out of the query's scope and the test passes having asserted nothing.
That is not hypothetical — the fixture did exactly that twice while it was
being written, once printing
found nothing to extractand stillPASSED.Nothing was running these tests
ci-codeql-security.ymlbuilds a database and itsanalyzejob is gatedbehind the
codeql-readylabel, so the existingunbounded-profile-looptesthad never run in CI either.
ci-codeql-query-tests.ymlruns them on any PRtouching
.github/codeql-queries/**; it needs no database and no build(~3 min, dominated by query compilation). It is a separate workflow rather
than a job in
ci-codeql-security.ymlbecause that workflow'spull_requesttrigger is
types:[labeled]; adding the tests there would have meant wideningits triggers for every PR.
Two deliberate differences from the sibling workflows:
.qlref, sothe extract-nothing failure cannot pass silently again;
codeqlalready on the runner, because
.expectedasserts extractor line/columnlocations exactly and an unpinned CLI could flip these tests for reasons
unrelated to the diff.
docs/codeql.mdnamed two bundle pin sites; this makes three, so the doc andthe new workflow's header now name all three.
Also gitignores
*.testproj/, the databasescodeql test runextracts besideeach test directory. They were never ignored, so running the tests left
untracked directories behind.
Verification
<=/ shipped).the
.expectedlocations are version-stable.preflight-safety-checks.sh: 0 failures.yamllinton the new workflowreports the same two warnings
ci-codeql-security.ymlreports(
document-start,truthy) and nothing else.