implicit-contract · git:20261014.7fe7a29 · 2026-10-14 · sha256 9582208f11397ea1
implicit-contract git:20261014.7fe7a29A
Immutable. This exact content never changes and is served at /api/v1/blob/9582208f11397ea1.
---
name: implicit-contract
description: Classify implicit-contract evidence — a caller/framework-supplied struct field is dereferenced with no dominating null guard, but the code null-checks the field elsewhere. Maps to CWE-20.
license: MIT
compatibility: opencode,claude code,DSH
metadata:
cwe: CWE-20
severity: MEDIUM
domain: contract
---
## Implicit Contract — Missing Entry Validation Analysis (CWE-20)
`contract_kind: implicit` — the field's nullness is **open-world** (supplied by a
caller or a framework the scanner cannot see), not statically provable. The
pipeline deliberately **never auto-confirms** this type: it only surfaces a
candidate when the code itself treats the field as nullable (it null-checks the
field on another path) but fails to guard THIS dereference. You judge the
business contract, not a data-flow proof.
A confirmed finding here is a **null-deref in spirit (CWE-476-equivalent)** when
the empty/unusual path can actually occur; it is reported under CWE-20 because
the root cause is *missing validation of input state*, and the field's nullness
is a contract, not a proven source.
### Evidence Pattern
An implicit-contract candidate has:
- **contract_gap**: a pointer field `obj->P` (of struct parameter `obj`) is
dereferenced at `use_line` with no dominating null guard.
- **missing**: the absent precondition (`obj->P != NULL`).
- **contract_evidence** (one of):
- `self` — the function null-checks `obj->P` on another path;
- `sibling` — another function with the SAME parameter struct type
null-checks the field (type-anchored cross-function contract);
- (caller validation is a *suppression*: if every caller null-checks the field
before the call, the candidate is dropped, not surfaced).
- **count_field** (optional): when a count/length field `obj->N` gates the use
(loop bound / subscript index), it is recorded as extra context — the pointer
is a counted collection. It is NOT required: the pointer-null condition is
caught independently of any count relationship.
- **call_path**: the function is reachable from an entry point.
### Detection Logic
1. Identify struct-pointer parameters whose fields are **read but never written**
in the function (a field written in-scope belongs to null-deref, not here).
2. Find a pointer field `obj->P` that is dereferenced (directly or via a local
`p = obj->P`).
3. Require **E-b self-guard evidence**: the field (or its derived local) is
null-checked somewhere in the function — without this, the field is treated
non-null by convention and no candidate is emitted.
4. Emit one `CONTRACT_GAP` (`category: ptr_guard`) per dereference site. The
planner filter drops a site when that null-check **dominates** it on the CFG
(properly guarded), so only the unguarded sites survive as `suspected`.
### Classification Rules
| Condition | Classification |
|-----------|---------------|
| The field can be NULL on a reachable path (caller passes an empty/failed result, framework deserializes optional data) and the dereference runs → crash | **confirmed** |
| A caller or the framework guarantees the field is non-null by protocol (e.g. a message handler that only runs on a validated non-empty message) | **false-positive** (contract guarantees non-null) |
| The count field `obj->N` is guaranteed positive when the field is used, or the dereference is inside a loop that no-ops on count==0 and the field is only touched when count>0 | **false-positive** (empty state is handled) |
| The caller already validates `obj->P != NULL` before every call, and the function is only called from those sites | **false-positive** (validated at the call boundary) |
| The field is a fixed array / interior pointer that can never be NULL (`&x`, string literal, offsetof-style accessor) | **false-positive** (non-nullable by construction) |
| The evidence is name-based only (fields merely share a `data`/`num` prefix with no usage coupling), or the source is unreadable | **dismissed** (insufficient evidence) |
### Common False Positives
- Framework/message handlers where the runtime guarantees the field is non-null
for the message type being handled (IPC/callback contract).
- A counted loop `for (i = 0; i < obj->n; i++) use(obj->p[i])` where `obj->n == 0`
is a legal empty result and the loop simply does not run.
- The caller constructs the object and fills the field before calling, so the
field is non-null at every call site.
- The field is never actually NULL in practice — the null-check on another path
is defensive, not evidence of a reachable NULL.
### Fix Suggestions
- Validate the field before the dereference, mirroring the codebase's own
convention for that field: `if (obj->P == NULL) { <log/return>; }`.
- If an empty result is legal, handle it explicitly (early return / skip) rather
than dereferencing; do not invent a return code — match the function's existing
error/empty conventions.
### Severity Matrix
`status` and `severity` are independent: `status` = evidence verdict
(confirmed/dismissed), `severity` = impact (low/medium/high/critical).
`metadata.severity` is the type default, not a ceiling. The verdict is binary
(confirmed/dismissed); dismissed → `low`.
| Shape | Severity |
|-------|----------|
| Field dereference on an attacker/externally-influenced empty path (untrusted message, deserialized buffer) → reachable crash | HIGH |
| Field dereference on an internal/rare empty path (edge case, low exposure) | MEDIUM |
| Non-reachable defensive path, or evidence too weak to settle | LOW (dismissed) |