Skip to content

checklocks: fix panic on cross-package use of unexported global guards - #14078

Open
tigerquoll wants to merge 1 commit into
google:masterfrom
tigerquoll:checklocks-global-guard-fix
Open

checklocks: fix panic on cross-package use of unexported global guards#14078
tigerquoll wants to merge 1 commit into
google:masterfrom
tigerquoll:checklocks-global-guard-fix

Conversation

@tigerquoll

Copy link
Copy Markdown

Problem

Any checklocks annotation whose guard is an unexported package-level variable panics the analyzer when the annotated function is called — or the annotated field is accessed — from a different package:

panic: interface conversion: interface is nil, not ssa.Value
    gvisor.dev/gvisor/tools/checklocks.(*globalGuard).resolveCommon(...)
        tools/checklocks/facts.go:208

Both resolution paths are affected — resolveCall (via checkFunctionCall) and resolveField (via checkGuards/checkFieldAccess). Minimal repro matrix (two packages: the guard-defining one and a caller):

guard call annotation (+checklocksexclude: etc.) field annotation (+checklocks:)
unexported global panic panic
exported global works, enforces works, enforces

Root cause

resolveCommon does pkg.Members[g.ObjectName].(ssa.Value) unchecked. For a cross-package use site, the guard package's SSA is created from export data, which does not include unexported package-level variables — the member lookup returns nil and the type assertion panics. (The interface-conversion, rather than nil-pointer, panic shows the package itself resolved; only the member is missing.)

History: #7721 (2022) fixed the exported cross-package case by adding the ImportedPackage lookup, but the tests it added only cover exported guards. The field-path variant of this panic has been latent since then; the call-path variant became commonly reachable when checklocksexclude{,write} landed (#12439).

Fix

Resolution now yields an explicitly unavailable resolvedValue when the guard object has no member in the resolving package, and the five use sites skip such guards instead of enforcing, reporting, or panicking. unavailable is deliberately distinct from the existing invalid state: invalid values are reported as "field %s cannot be resolved" in some paths and panic in lockState.isHeld/lockField in others, and neither is right here — the annotation itself is valid and remains fully enforced inside its defining package, so a use site that cannot materialize the object should be silent.

Skipping is sound: a consuming package could only hold the unresolvable guard via an acquire-annotated API whose fact references the same unresolvable object, so the acquisition could never enter its lock state either — the skipped check is vacuously unviolatable. This matches the analyzer's existing elision behavior (newly allocated objects, closures). In checkGuards, an unavailable guard is also excluded from guardsFound so it cannot silently relax a mixed +checkatomic field.

The README now documents the limitation in both places that list global locks as resolvable: an unexported global lock is enforceable only within its declaring package; export the lock if cross-package enforcement is required.

Tests

test/crosspkg + test/globals.go gain unexported-global-guard coverage for both paths, with +checklocksfail expectations verified load-bearing (removing them makes the analyzer report missing expected failure). Before the fix, the new tests reproduce the panic; after it, the analyzer's output over the whole test tree is otherwise identical to the pre-change baseline, and the exported cross-package cases still enforce.

Generated by the Author with assistance from Claude Code

@google-cla

google-cla Bot commented Aug 12, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@github-actions
github-actions Bot requested review from carzh and shailend-g August 12, 2026 05:07
An annotation whose guard is an unexported package-level variable panicked
the analyzer whenever the annotated function was called, or the annotated
field accessed, from another package. Export data does not include
unexported package-level variables, so the guard package's SSA members do
not contain the object and the unchecked type assertion in resolveCommon
failed.

Resolution now yields an explicitly unavailable resolvedValue in that case,
and the guard is skipped at the use site rather than enforced or reported.
The annotation itself remains valid, and is still enforced within the
package that declares the guard. Exported globals are unaffected. The
README documents the limitation, since silently skipping the guard leaves
documentation as the channel for guiding annotation authors.
@tigerquoll
tigerquoll force-pushed the checklocks-global-guard-fix branch from 4f07cea to 2d29af2 Compare August 12, 2026 05:45
tigerquoll added a commit to tigerquoll/yunikorn-k8shim that referenced this pull request Aug 12, 2026
The analyser is gVisor's tools/checklocks, which gVisor does not publish as
an importable module: the previous pin reached into the gvisor repository and
took the whole tree with it. github.com/tigerquoll/checklocks is a standalone
extraction of the analyser from gvisor commit 1919d963 with three fixes that
these annotations need, the first of them filed upstream as google/gvisor#14078.
Its own README calls it a temporary home, so this pin moves again once the
fixes land upstream.

This unblocks the branch: with the previous pin "make checklocks" panicked on
pkg/shim, which calls the annotated NewPlaceholderManager from another package.
It now passes.

The dispatcher regains the negative preconditions on its self locking package
functions. The fix for pointer typed global guards makes the annotation and the
acquisition resolve to the same lock, so a caller in this package that holds
the dispatcher lock directly is now reported. A caller that reaches the lock
through getDispatcher, and any caller outside the package, still cannot be
checked; both are recorded at the declaration site.

The tools module go directive follows the analyser at 1.25.0, below the shim
toolchain, so building the tool can no longer trigger a toolchain switch. The
guard on the toolchain stays: what it protects against is the tool and go vet
running different toolchains.
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.

1 participant