security: triage the five CodeQL alerts, two of which are real - #122
Conversation
…lamp is holding up CodeQL's first run on this repository reported five high-severity alerts. Two of them are real, and both are here. UHP_MAX_CONCURRENT_RUNS and UHP_TASK_MAX_STEP are parsed by strconv.ParseInt into an int64 and were then converted straight to the int their Config fields are declared as. int is as wide as the machine, so on the amd64 and arm64 builds this project ships that conversion is the identity and the alert reads as noise. It is not noise on a 32-bit build, which `go install` produces happily and nothing here argues against — an armv7 box is a plausible place to run a small daemon. There the high half is dropped in silence: UHP_TASK_MAX_STEP=4294967296 arrives as 0 and 2147483648 arrives negative, and zero and negative are both how TaskMaxStep and MaxConcurrentRuns spell "no limit of this deployment's own". An operator reaching for an enormous ceiling would get none at all, which is the direction #72 says a bound must never fail in. Both settings now go through getEnvIntCapped, which saturates at math.MaxInt32 on every platform rather than at math.MaxInt. Saturating at a fixed number is what makes the answer identical on both word sizes, and that sameness is the point: a setting whose meaning depends on the architecture it was compiled for is a worse thing to reason about than one that is merely bounded. The bound costs a deployment nothing it could have wanted, because neither setting means anything at two billion concurrent harness processes or two billion agent steps in one task. MaxBodyBytes keeps getEnvInt, its field being an int64 already. The regression test cannot observe the truncation, because the machine running it converts without narrowing. What it pins instead is the cap, which is the property that makes the two platforms agree; it failed on three of its five cases before the change, each returning the unclamped value. The third alert is the store's page allocation, and it is a false positive — but only because of four lines that had nothing saying so. `?limit=` is read off the query string with strconv.Atoi and passed to the store with no range applied at the transport, so CodeQL is right about the source: the value is a stranger's. ListSessions clamps it to [1,100] before it reaches make(), which is what makes the allocation bounded, and the clamp read like a default page size rather than like the thing standing between a request and eight gigabytes of preallocated pointers. It now says which it is, so that a later refactor moving it, or widening it to take the caller's number, has to argue with a comment first. The remaining two alerts are dismissed via the API with their reasoning; CodeQL for Go has no inline suppression to leave at the site. Verified with `make verify` and `make security-push`: golangci-lint 0 issues, the full suite green under -race, gosec and gitleaks clean. govulncheck's stdlib findings are against the local 1.25.0 toolchain and unrelated; CI builds with 1.26.
|
All five are now resolved: #3 and #4 closed as Should CodeQL start failing the build?Yes — the reason it does not is spent. The workflow comment says the backlog is why findings go to the Security tab rather than the build, and that it should gate "once the backlog is triaged". The backlog is zero, so gating from here can only break on something new, which is the signal worth having and not the day-one red build the comment was avoiding. gosec already hard-fails in Three things to get right when doing it, none of which are a workflow edit: It is a ruleset setting, not Set the threshold at security-severity high, not at all alerts. All five triaged here were high. Dropping the bar to note-level would put the tone-ish Go queries in front of every PR, which is how a gate gets switched off. The false-positive rate is the ongoing cost, and it is not small. Three of five were false positives, and Go has no inline suppression — every one costs a Not changed here, per the scope of this PR. Related: #119. |
|
Follow-up: the gate is now live, so the comment above is history rather than a proposal. One correction to what it said. Gating is a rulesets feature and this repository had no rulesets — Ruleset Verified rather than assumed, in #123: a branch introducing one deliberate
|
CodeQL's first run reported five high-severity alerts. Two are real, three are false
positives. Each verdict below is about where the value comes from, not about how noisy the
rule is in general.
Real:
go/incorrect-integer-conversionatinternal/config/config.go:115and:122(#3, #4)UHP_MAX_CONCURRENT_RUNSandUHP_TASK_MAX_STEPare parsed withstrconv.ParseIntintoan
int64and were converted straight to theinttheirConfigfields are declared as.The value is operator-controlled — an environment variable read at startup — so no
privilege boundary is crossed and this is not an attack path. It is still a real defect,
because
intis as wide as the machine: the conversion is the identity on the amd64 andarm64 builds this project ships, and a silent truncation on a 32-bit build, which
go installproduces happily and nothing here argues against.The failure direction is what makes it worth fixing rather than filing. On 32-bit,
UHP_TASK_MAX_STEP=4294967296keeps only its low half and arrives as0, and2147483648arrives negative — and zero and negative are both howTaskMaxStepandMaxConcurrentRunsspell no limit of this deployment's own. An operator reaching for anenormous ceiling would get none at all, which is the direction #72 says a bound must never
fail in.
Both now go through
getEnvIntCapped, saturating atmath.MaxInt32on every platformrather than at
math.MaxInt, so the setting means the same thing regardless of the wordsize it was compiled for.
MaxBodyByteskeepsgetEnvInt, its field beingint64.The regression test cannot observe the truncation on the machine running it, so it pins
the cap instead — the property that makes the two platforms agree. It fails on three of
its five cases before the change, each returning the unclamped value.
False positive:
go/uncontrolled-allocation-sizeatinternal/store/sqlite.go:532(#5)CodeQL is right about the source.
?limit=is read off the query string withstrconv.AtoiinhandleListSessions, the error is discarded, and it reaches the storewith no range applied at the transport — the value is a stranger's, and it sizes a
make().It is nonetheless bounded.
ListSessionsclamps to[1,100]in its first four lines,limitis not reassigned between there and the allocation, and the worst case is 100pointers. Both stores clamp identically. What CodeQL misses is that the sanitiser is a
reassignment inside the
ifrather than an early return, so the path where the value isalready in range is followed without recording that being in range is what put it there.
No code change, but the clamp now carries a comment: it read like a default page size,
when it is in fact the only thing between one request and eight gigabytes of preallocated
pointers. A later refactor widening it has to argue with that comment first.
False positive:
go/allocation-size-overflowatuhp/uhpgo/uhpgo.go:120(#1, #2)make([]byte, 0, len(wire)+len(ext)), one alert per operand. Both operands are thelengths of
[]bytevaluesjson.Marshalhas already returned and which are live inmemory at that line. Two allocations that already succeeded cannot sum past the integer
type that measures either one without the process having exhausted its address space
first — the arithmetic is bounded by the allocator, whoever controls the harness content.
Even granting the impossible, a negative capacity panics in
make; it does notunder-allocate, so there is no memory-safety consequence on the other side either. No
inline suppression exists for Go, so these are dismissed via the API with that reasoning.
Verification
make verify(golangci-lint 0 issues, full suite green under-race, script tests, build)and
make security-push(gosec and gitleaks clean). govulncheck's stdlib findings areagainst the local 1.25.0 toolchain and unrelated; CI builds with 1.26.
Whether CodeQL should start gating is a separate question, answered in a comment here once
this run is green rather than changed in this PR.