Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
/wormchain/devnet/txverifier @djb15 @johnsaigle @mdulin2 @pleasew8t
/wormchain/ts-sdk/ @evan-gray @kev1n-peters @panoel
/linters/ @djb15 @johnsaigle @mdulin2 @pleasew8t @bemic
/codeql/ @djb15 @johnsaigle @mdulin2 @pleasew8t @bemic

# Protobuf for node

Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: CodeQL

on:
push:
branches: [ main ]
paths:
- "node/**"
- "sdk/**/*.go"
- "sdk/**/go.mod"
- "sdk/**/go.sum"
- "codeql/**"
- ".github/workflows/codeql.yml"
pull_request:
branches: [ main ]
paths:
- "node/**"
- "sdk/**/*.go"
- "sdk/**/go.mod"
- "sdk/**/go.sum"
- "codeql/**"
- ".github/workflows/codeql.yml"
workflow_dispatch:

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
# Compile the custom query pack and run its unit test fixtures.
query-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.25.10"
- name: Install CodeQL CLI
env:
GH_TOKEN: ${{ github.token }}
run: |
gh extension install github/gh-codeql
gh codeql version
- name: Install query pack dependencies
env:
GH_TOKEN: ${{ github.token }}
working-directory: codeql
run: gh codeql pack install
- name: Compile queries
env:
GH_TOKEN: ${{ github.token }}
working-directory: codeql
run: gh codeql query compile src
- name: Run query tests
env:
GH_TOKEN: ${{ github.token }}
working-directory: codeql
run: gh codeql test run test

# Analyze the node and sdk Go modules with the custom query suite and
# upload the results to GitHub code scanning.
analyze:
strategy:
fail-fast: false
matrix:
module: [node, sdk]
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.25.10"
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
languages: go
build-mode: manual
config: |
disable-default-queries: true
paths:
- ${{ matrix.module }}
paths-ignore:
- "**/*_test.go"
queries:
- uses: ./codeql/suites/wormhole-go.qls
# CodeQL extracts Go code while it is compiled, so build only the module
# under analysis. -a forces a full rebuild so every file is extracted.
- name: Build Go module
env:
MODULE: ${{ matrix.module }}
run: cd "$MODULE" && go build -a ./...
- name: Analyze
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
category: "wormhole-codeql/${{ matrix.module }}"
4 changes: 4 additions & 0 deletions codeql/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# CodeQL output
*.sarif
# CodeQL databases
*-db/
73 changes: 73 additions & 0 deletions codeql/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# The pack lives inside the Wormhole monorepo, so the source root defaults to the parent directory.
WORMHOLE_REPO ?= ..
NODE_DB ?= ./wormhole-node-go-db
SDK_DB ?= ./wormhole-sdk-go-db
SUITE ?= suites/wormhole-go.qls
NODE_OUTPUT ?= wormhole-node-go-lints.sarif
SDK_OUTPUT ?= wormhole-sdk-go-lints.sarif

.PHONY: all
all: analyze

.PHONY: create-db create-node-db create-sdk-db
create-db: create-node-db create-sdk-db

create-node-db:
codeql database create $(NODE_DB) \
--language=go \
--source-root=$(WORMHOLE_REPO) \
--command='/bin/sh -c "cd node && go build -a ./..."'

create-sdk-db:
codeql database create $(SDK_DB) \
--language=go \
--source-root=$(WORMHOLE_REPO) \
--command='/bin/sh -c "cd sdk && go build -a ./..."'

.PHONY: create-db-overwrite create-node-db-overwrite create-sdk-db-overwrite
create-db-overwrite: create-node-db-overwrite create-sdk-db-overwrite

create-node-db-overwrite:
codeql database create $(NODE_DB) \
--language=go \
--source-root=$(WORMHOLE_REPO) \
--command='/bin/sh -c "cd node && go build -a ./..."' \
--overwrite

create-sdk-db-overwrite:
codeql database create $(SDK_DB) \
--language=go \
--source-root=$(WORMHOLE_REPO) \
--command='/bin/sh -c "cd sdk && go build -a ./..."' \
--overwrite

.PHONY: analyze analyze-node analyze-sdk
analyze: analyze-node analyze-sdk

analyze-node:
codeql database analyze $(NODE_DB) \
$(SUITE) \
--format=sarif-latest \
--output=$(NODE_OUTPUT)

analyze-sdk:
codeql database analyze $(SDK_DB) \
$(SUITE) \
--format=sarif-latest \
--output=$(SDK_OUTPUT)

.PHONY: scan
scan: create-db-overwrite
$(MAKE) analyze

.PHONY: compile
compile:
codeql query compile src

.PHONY: test
test:
codeql test run test

.PHONY: clean
clean:
rm -rf $(NODE_DB) $(SDK_DB) $(NODE_OUTPUT) $(SDK_OUTPUT)
107 changes: 107 additions & 0 deletions codeql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Wormhole Go CodeQL Lints

This directory is a CodeQL query pack with Wormhole-specific Go lint rules. Run the commands below from this directory (`codeql/`). The pack lock file (`codeql-pack.lock.yml`) pins the Go query dependencies; run `codeql pack install` once to download them.

CI compiles the queries, runs the query unit tests, and analyzes the `node` and `sdk` Go modules with this pack (see `.github/workflows/codeql.yml`). Findings are uploaded to GitHub code scanning and appear as annotations on pull requests.

## Rules

- [`wormhole/go/already-locked-receiver-mutex`](docs/rules/already-locked-receiver-mutex.md): require documented `AlreadyLocked` helpers to be called while holding the exact receiver mutex.
- [`wormhole/go/algorand-publication-field-length-check`](docs/rules/algorand-publication-field-length-check.md): require exact length checks before decoding Algorand publication nonce and sequence fields.
- [`wormhole/go/canonical-chain-id-parsing`](docs/rules/canonical-chain-id-parsing.md): use Wormhole SDK chain-ID conversion helpers at modeled external boundaries.
- [`wormhole/go/canonical-vaa-address-parsing`](docs/rules/canonical-vaa-address-parsing.md): use Wormhole SDK address parsers for external address values.
- [`wormhole/go/canonical-vaa-id-parsing`](docs/rules/canonical-vaa-id-parsing.md): parse complete VAA IDs with the canonical parser instead of reconstructing components manually.
- `wormhole/go/delegate-consensus-canonical-digest`: key delegate observation quorum buckets by the reconstructed `MessagePublication` VAA signing digest, not by serialized observations or composite keys.
- `wormhole/go/delegated-guardian-config-validation`: strictly parse guardian addresses, reject duplicate canonical keys, and enforce non-empty threshold/quorum before governance serialization.
- [`wormhole/go/evm-finality-release-and-reorg-checks`](docs/rules/evm-finality-release-and-reorg-checks.md): require finality, receipt refetch, and reorg-provenance checks before releasing pending EVM observations.
- [`wormhole/go/evm-require-successful-receipt-before-observation`](docs/rules/evm-require-successful-receipt-before-observation.md): require a local successful-receipt proof before EVM log observation or publication.
- [`wormhole/go/evm-verify-and-publish-gate`](docs/rules/evm-verify-and-publish-gate.md): route EVM watcher publication through `verifyAndPublish`.
- `wormhole/go/evm-ccl-signed-message-immutability`: preserve signed `MessagePublication` fields after observation and update only release metadata such as `effectiveCL` or `additionalBlocks`.
- `wormhole/go/governance-vaa-typed-payload`: production governance VAA construction must pass `CreateGovernanceVAA` a payload from a checked SDK typed governance serializer or `EmptyPayloadVaa`.
- `wormhole/go/guardian-signer-exact-digest-length`: `GuardianSigner.Sign` implementations must reject non-32-byte digest input before signing.
- [`wormhole/go/message-publication-canonical-timestamp`](docs/rules/message-publication-canonical-timestamp.md): use `vaa.TimeFromUnix` for modeled chain-derived publication timestamps.
- [`wormhole/go/message-publication-safe-serialization`](docs/rules/message-publication-safe-serialization.md): avoid deprecated publication serialization helpers that omit security-relevant fields.
- [`wormhole/go/near-finalized-receipt-outcome-before-publication`](docs/rules/near-finalized-receipt-outcome-before-publication.md): require same-outcome NEAR finality and finalized-header provenance before processing receipt logs for publication.
- [`wormhole/go/run-with-scissors-error-return`](docs/rules/run-with-scissors-error-return.md): return runnable errors instead of writing directly to the same `errC`.
- `wormhole/go/solana-alt-owner-before-decode`: prove an RPC-fetched address lookup table account exists and is owned by the ALT program before decoding its bytes.
- [`wormhole/go/solana-commitment-match-before-publication`](docs/rules/solana-commitment-match-before-publication.md): require decoded Solana message commitment to match the exact watcher before scheduling or publication.
- [`wormhole/go/solana-message-account-validation`](docs/rules/solana-message-account-validation.md): require validated constructor provenance for Solana message account data.
- [`wormhole/go/solana-require-successful-transaction-meta`](docs/rules/solana-require-successful-transaction-meta.md): require successful Solana transaction metadata before parsing or processing observations.
- `wormhole/go/untrusted-vaa-use-before-verification`: parsed signed VAAs from untrusted boundaries must be verified with the complete guardian set before storage or external delivery; `vaa.Unmarshal` only checks wire format.
- [`wormhole/go/xrpl-derived-generated-emitter`](docs/rules/xrpl-derived-generated-emitter.md): derive collision-resistant emitters for XRPL-generated messages.
- [`wormhole/go/xrpl-first-memo-only`](docs/rules/xrpl-first-memo-only.md): inspect only the first XRPL memo for Wormhole Core and NTT messages.
- [`wormhole/go/xrpl-require-validated-transaction`](docs/rules/xrpl-require-validated-transaction.md): require a validated-ledger proof before parsing an XRPL transaction.

## Compile

Compile every query in the pack:

```sh
codeql query compile src
```

Compile the registered suite:

```sh
codeql query compile suites/wormhole-go.qls
```

## Test

Run every rule fixture:

```sh
codeql test run test
```

Run one rule fixture:

```sh
codeql test run test/xrpl-first-memo-only
```

## Analyze Wormhole

Create separate Go databases for `node` and `sdk`. CodeQL extracts Go code while the database is created, so use a single source root with subdirectory-scoped build commands rather than creating one database from the whole repository. This avoids unrelated SDK subdirectories such as `sdk/js`, `sdk/js-proto-node`, `sdk/js-proto-web`, `sdk/js-wasm`, and `sdk/rust`.

Create the `node` database:

```sh
codeql database create wormhole-node-go-db \
--language=go \
--source-root=.. \
--command='cd node && go build -a ./...' \
--overwrite
```

Create the Go SDK database:

```sh
codeql database create wormhole-sdk-go-db \
--language=go \
--source-root=.. \
--command='cd sdk && go build -a ./...' \
--overwrite
```

Analyze both databases with the registered suite:

```sh
codeql database analyze wormhole-node-go-db \
suites/wormhole-go.qls \
--format=sarif-latest \
--output=wormhole-node-go-lints.sarif

codeql database analyze wormhole-sdk-go-db \
suites/wormhole-go.qls \
--format=sarif-latest \
--output=wormhole-sdk-go-lints.sarif
```

If you already have finalized databases, skip the `database create` commands and run `database analyze` against those database paths.

Alternatively, use the `Makefile`, which defaults the source root to the enclosing repository:

```sh
make scan
```
24 changes: 24 additions & 0 deletions codeql/codeql-pack.lock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
lockVersion: 1.0.0
dependencies:
codeql/concepts:
version: 0.0.26
codeql/controlflow:
version: 2.0.36
codeql/dataflow:
version: 2.1.8
codeql/go-all:
version: 7.2.0
codeql/mad:
version: 1.0.52
codeql/ssa:
version: 2.0.28
codeql/threat-models:
version: 1.0.52
codeql/tutorial:
version: 1.0.52
codeql/typetracking:
version: 2.0.36
codeql/util:
version: 2.0.39
compiled: false
55 changes: 55 additions & 0 deletions codeql/docs/rules/algorand-publication-field-length-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Algorand Publication Field Length Check

Prove Algorand `publishMessage` nonce and sequence byte fields are exactly 8 bytes before decoding them with `binary.BigEndian.Uint64`.

## Why This Matters

Algorand watcher code converts chain data into canonical Wormhole `MessagePublication` values. The publication nonce comes from `ApplicationArgs[2]`, and the sequence comes from the first log entry. `binary.BigEndian.Uint64` requires an 8-byte input; malformed field lengths can panic the watcher before the observation is skipped. Container bounds, app ID, method-name checks, and contract-side `Itob` expectations do not prove the exact byte width of the field being decoded.

## Examples

### Violation

```go
func build(at ApplicationTransaction, ed EvalDelta) MessagePublication {
nonce := binary.BigEndian.Uint64(at.ApplicationArgs[2])
sequence := binary.BigEndian.Uint64([]byte(ed.Logs[0]))
return MessagePublication{Nonce: uint32(nonce), Sequence: sequence}
}
```

### Fix

```go
func build(at ApplicationTransaction, ed EvalDelta) (MessagePublication, bool) {
if len(at.ApplicationArgs[2]) != 8 || len([]byte(ed.Logs[0])) != 8 {
return MessagePublication{}, false
}
nonce := binary.BigEndian.Uint64(at.ApplicationArgs[2])
sequence := binary.BigEndian.Uint64([]byte(ed.Logs[0]))
return MessagePublication{Nonce: uint32(nonce), Sequence: sequence}, true
}
```

## What The Rule Checks

The rule reports production Go under `node/pkg/watchers/algorand/` and `pkg/watchers/algorand/` when an Algorand publication function decodes `at.ApplicationArgs[2]` or `[]byte(ed.Logs[0])` with `binary.BigEndian.Uint64` without a dominating exact `len(value) == 8` proof for the same value.

It recognizes direct `binary.BigEndian.Uint64(...)` calls, local aliases of `binary.BigEndian`, local aliases of the nonce or sequence bytes, stale guards invalidated by reassignment of the alias or exact indexed source, and thin local helper calls whose parameter reaches an internal `Uint64`. A checked helper is safe only when the helper enforces exact length and every relevant helper result published into `MessagePublication.Nonce` or `MessagePublication.Sequence` is dominated by rejection of the helper error. The rule ignores tests, generated files, non-Algorand watchers, non-publication decodes, container-bounds-only code, and typed/fixed-width values already produced by checked parsers.

## Limitations

The model is intentionally bounded to the two Wormhole Algorand publication fields and thin local helpers in watcher code. Deep interprocedural propagation, non-local abstractions, equivalent checked parsers with different shapes, and publication construction hidden behind complex containers may be missed. The query proves dominance syntactically with current guard forms; unusual but safe control flow may need additional fixtures before being accepted.

## Learn More

- [Rule contract](../../../.codeql-lint-builder/rules/algorand-publication-field-length-check.md): records the exact field definitions, guard contract, helper treatment, bypass risks, tests, and calibration evidence.
- [Second-gate return report](../../../.codeql-lint-builder/runs/algorand-publication-field-length-check/09-second-gate-return-2026-07-15.md): records the final checked-helper publication-use fix and zero-result recalibration.
- [Rule query](../../src/algorand-publication-field-length-check.ql): defines exact field sources, `Uint64` sinks, dominance, stale-guard invalidation, and thin-helper modeling.
- [Rule fixtures](../../test/algorand-publication-field-length-check/): encode unguarded direct decodes, non-exact checks, stale guards, `BigEndian` aliases, helper bypasses, checked-helper error handling, and exclusions.

The rule artifact cites Wormhole Algorand watcher source, malformed-length regression tests, contract code, and hardening commit `5ce968ff1638d353f9bfe8c94461f9583eaeeedf`, but this checkout does not contain the Wormhole repository, so this page cannot provide verified version-pinned links to them.

## Maintainer Notes

The CodeQL ID is `wormhole/go/algorand-publication-field-length-check`. Update query and fixtures together if Algorand watcher paths, nonce/sequence source expressions, publication construction, helper return conventions, or accepted exact-length guard shapes change.
Loading
Loading