Skip to content

[analyzer] Recognize missing MPI collectives in MPI-Checker#209263

Open
steakhal wants to merge 1 commit into
llvm:mainfrom
steakhal:bb/mpi-checker-missing-collectives
Open

[analyzer] Recognize missing MPI collectives in MPI-Checker#209263
steakhal wants to merge 1 commit into
llvm:mainfrom
steakhal:bb/mpi-checker-missing-collectives

Conversation

@steakhal

@steakhal steakhal commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Fixes a false positive in the optin.mpi.MPI-Checker where legitimate code was flagged with "Request 'X' has no matching nonblocking call."

Background for reviewers unfamiliar with MPI or this checker

MPI (Message Passing Interface) is the standard API for communication between processes in HPC/cluster programs (C/C++/Fortran). Many MPI operations come in a nonblocking form, prefixed with "I" (for "immediate"): e.g. MPI_Isend, MPI_Iscatter. A nonblocking call returns immediately and hands back an MPI_Request handle; the caller must later complete it with MPI_Wait (or MPI_Waitall). Forgetting to wait, or reusing a request before waiting, are real bugs (buffer reuse races, leaked requests).

The MPI-Checker (originally contributed as a GSoC 2016 project, and is a domain-specific check - hence it's in "optin").
It tracks each MPI_Request region through a small state machine and reports: three misuse patterns:

  • missing wait: a nonblocking call whose request is never waited on
  • double nonblocking: a request reused by a second nonblocking call before being waited on
  • unmatched wait: MPI_Wait on a request with no originating nonblocking call

Root cause

MPIFunctionClassifier maintains a hand-written list of known MPI functions. That list was incomplete: several standard collectives -- notably the vector variants (MPI_Iscatterv, MPI_Igatherv, ...), the scans, the reduce-scatter family, and MPI_Ibarrier -- were entirely absent. Because isNonBlockingType() returned false for them, checkDoubleNonblocking() never recorded the request as Nonblocking, so the matching MPI_Wait looked unmatched and produced a FP.

Note that only isNonBlockingType()/isWaitType()/isMPI_Wait()/isMPI_Waitall() actually drive the checker; the per-shape predicates (isScatterType, isGatherType, isCollToColl, etc.) and their backing containers are currently unused. checkDoubleNonblocking() also reads the request from the last argument, which is generic since every nonblocking MPI function places MPI_Request* last -- so no per-function argument handling is needed.

Fix

Register the remaining standard collectives and mark the nonblocking ("I") variants as nonblocking:

  • vector variants: MPI_Scatterv/Iscatterv, MPI_Gatherv/Igatherv, MPI_Allgatherv/Iallgatherv, MPI_Alltoallv/Ialltoallv, MPI_Alltoallw/Ialltoallw
  • nonblocking barrier: MPI_Ibarrier
  • scans: MPI_Scan/Iscan, MPI_Exscan/Iexscan
  • reduce-scatter: MPI_Reduce_scatter/Ireduce_scatter, MPI_Reduce_scatter_block/Ireduce_scatter_block

These functions are not referenced by any per-shape predicate, so they are added directly to the classification containers without a dedicated IdentifierInfo member. Neighborhood collectives (MPI_Ineighbor_*) and the wait-family beyond Wait/Waitall (Waitany/Waitsome/Test*) remain out of scope.

Adds regression tests covering the reported repro (no warning), and confirms double-nonblocking and missing-wait are still detected for the new functions.

Fixes #208107

Fixes a false positive in the optin.mpi.MPI-Checker where legitimate code
was flagged with "Request 'X' has no matching nonblocking call."

Background for reviewers unfamiliar with MPI or this checker
------------------------------------------------------------

MPI (Message Passing Interface) is the standard API for communication
between processes in HPC/cluster programs (C/C++/Fortran). Many MPI
operations come in a *nonblocking* form, prefixed with "I" (for
"immediate"): e.g. MPI_Isend, MPI_Iscatter. A nonblocking call returns
immediately and hands back an MPI_Request handle; the caller must later
complete it with MPI_Wait (or MPI_Waitall). Forgetting to wait, or reusing
a request before waiting, are real bugs (buffer reuse races, leaked
requests).

The MPI-Checker (originally contributed as a GSoC 2016 project, and is a
domain-specific check - hence it's in "optin").
It tracks each MPI_Request region through a small state machine and reports:
three misuse patterns:

  * **missing wait**: a nonblocking call whose request is never waited on
  * **double nonblocking**: a request reused by a second nonblocking call
    before being waited on
  * **unmatched wait**: MPI_Wait on a request with no originating
    nonblocking call

Root cause
----------

`MPIFunctionClassifier` maintains a hand-written list of known MPI functions.
That list was incomplete: several standard collectives -- notably the
vector variants (`MPI_Iscatterv`, `MPI_Igatherv`, ...), the scans, the
reduce-scatter family, and `MPI_Ibarrier` -- were entirely absent. Because
`isNonBlockingType()` returned `false` for them, `checkDoubleNonblocking()`
never recorded the request as Nonblocking, so the matching `MPI_Wait` looked
unmatched and produced a FP.

Note that only `isNonBlockingType()`/`isWaitType()`/`isMPI_Wait()`/`isMPI_Waitall()`
actually drive the checker; the per-shape predicates (`isScatterType`,
`isGatherType`, `isCollToColl`, etc.) and their backing containers are currently
unused. `checkDoubleNonblocking()` also reads the request from the *last*
argument, which is generic since every nonblocking MPI function places
`MPI_Request*` last -- so no per-function argument handling is needed.

Fix
---

Register the remaining standard collectives and mark the nonblocking ("I")
variants as nonblocking:

  * **vector variants**: `MPI_Scatterv`/`Iscatterv`, `MPI_Gatherv`/`Igatherv`,
    `MPI_Allgatherv`/`Iallgatherv`, `MPI_Alltoallv`/`Ialltoallv`,
    `MPI_Alltoallw`/`Ialltoallw`
  * **nonblocking barrier**: `MPI_Ibarrier`
  * **scans**: `MPI_Scan`/`Iscan`, `MPI_Exscan`/`Iexscan`
  * **reduce-scatter**: `MPI_Reduce_scatter`/`Ireduce_scatter`,
    `MPI_Reduce_scatter_block`/`Ireduce_scatter_block`

These functions are not referenced by any per-shape predicate, so they are
added directly to the classification containers without a dedicated
`IdentifierInfo` member. Neighborhood collectives (`MPI_Ineighbor_*`) and the
wait-family beyond `Wait`/`Waitall` (`Waitany`/`Waitsome`/`Test*`)
remain out of scope.

Adds regression tests covering the reported repro (no warning), and confirms
double-nonblocking and missing-wait are still detected for the new functions.

Fixes llvm#208107

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@steakhal steakhal requested review from NagyDonat and Xazax-hun July 13, 2026 18:20
@llvmorg-github-actions llvmorg-github-actions Bot added the clang Clang issues not falling into any other category label Jul 13, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-static-analyzer-1

Author: Balázs Benics (steakhal)

Changes

Fixes a false positive in the optin.mpi.MPI-Checker where legitimate code was flagged with "Request 'X' has no matching nonblocking call."

Background for reviewers unfamiliar with MPI or this checker

MPI (Message Passing Interface) is the standard API for communication between processes in HPC/cluster programs (C/C++/Fortran). Many MPI operations come in a nonblocking form, prefixed with "I" (for "immediate"): e.g. MPI_Isend, MPI_Iscatter. A nonblocking call returns immediately and hands back an MPI_Request handle; the caller must later complete it with MPI_Wait (or MPI_Waitall). Forgetting to wait, or reusing a request before waiting, are real bugs (buffer reuse races, leaked requests).

The MPI-Checker (originally contributed as a GSoC 2016 project, and is a domain-specific check - hence it's in "optin").
It tracks each MPI_Request region through a small state machine and reports: three misuse patterns:

  • missing wait: a nonblocking call whose request is never waited on
  • double nonblocking: a request reused by a second nonblocking call before being waited on
  • unmatched wait: MPI_Wait on a request with no originating nonblocking call

Root cause

MPIFunctionClassifier maintains a hand-written list of known MPI functions. That list was incomplete: several standard collectives -- notably the vector variants (MPI_Iscatterv, MPI_Igatherv, ...), the scans, the reduce-scatter family, and MPI_Ibarrier -- were entirely absent. Because isNonBlockingType() returned false for them, checkDoubleNonblocking() never recorded the request as Nonblocking, so the matching MPI_Wait looked unmatched and produced a FP.

Note that only isNonBlockingType()/isWaitType()/isMPI_Wait()/isMPI_Waitall() actually drive the checker; the per-shape predicates (isScatterType, isGatherType, isCollToColl, etc.) and their backing containers are currently unused. checkDoubleNonblocking() also reads the request from the last argument, which is generic since every nonblocking MPI function places MPI_Request* last -- so no per-function argument handling is needed.

Fix

Register the remaining standard collectives and mark the nonblocking ("I") variants as nonblocking:

  • vector variants: MPI_Scatterv/Iscatterv, MPI_Gatherv/Igatherv, MPI_Allgatherv/Iallgatherv, MPI_Alltoallv/Ialltoallv, MPI_Alltoallw/Ialltoallw
  • nonblocking barrier: MPI_Ibarrier
  • scans: MPI_Scan/Iscan, MPI_Exscan/Iexscan
  • reduce-scatter: MPI_Reduce_scatter/Ireduce_scatter, MPI_Reduce_scatter_block/Ireduce_scatter_block

These functions are not referenced by any per-shape predicate, so they are added directly to the classification containers without a dedicated IdentifierInfo member. Neighborhood collectives (MPI_Ineighbor_*) and the wait-family beyond Wait/Waitall (Waitany/Waitsome/Test*) remain out of scope.

Adds regression tests covering the reported repro (no warning), and confirms double-nonblocking and missing-wait are still detected for the new functions.

Fixes #208107


Full diff: https://github.com/llvm/llvm-project/pull/209263.diff

3 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp (+42)
  • (modified) clang/test/Analysis/MPIMock.h (+7)
  • (modified) clang/test/Analysis/mpichecker.cpp (+40)
diff --git a/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp b/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp
index 277b3ed2e1056..2d618c19b191f 100644
--- a/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/MPI-Checker/MPIFunctionClassifier.cpp
@@ -174,6 +174,48 @@ void MPIFunctionClassifier::initCollectiveIdentifiers(ASTContext &ASTCtx) {
   MPINonBlockingTypes.push_back(IdentInfo_MPI_Ialltoall);
   MPIType.push_back(IdentInfo_MPI_Ialltoall);
   assert(IdentInfo_MPI_Ialltoall);
+
+  // Register the remaining standard collectives. These are not referenced by
+  // any of the individual is<Kind>Type() predicates, so they don't need a
+  // dedicated IdentifierInfo member; the checker only relies on them being
+  // classified as collective / nonblocking. All nonblocking (MPI_I...)
+  // variants take an MPI_Request as their last argument.
+  auto AddCollective = [&](StringRef Name) {
+    IdentifierInfo *II = &ASTCtx.Idents.get(Name);
+    MPICollectiveTypes.push_back(II);
+    MPIType.push_back(II);
+    return II;
+  };
+  auto AddNonBlockingCollective = [&](StringRef Name) {
+    MPINonBlockingTypes.push_back(AddCollective(Name));
+  };
+
+  // Vector ("v"/"w") variants of the collectives above.
+  AddCollective("MPI_Scatterv");
+  AddNonBlockingCollective("MPI_Iscatterv");
+  AddCollective("MPI_Gatherv");
+  AddNonBlockingCollective("MPI_Igatherv");
+  AddCollective("MPI_Allgatherv");
+  AddNonBlockingCollective("MPI_Iallgatherv");
+  AddCollective("MPI_Alltoallv");
+  AddNonBlockingCollective("MPI_Ialltoallv");
+  AddCollective("MPI_Alltoallw");
+  AddNonBlockingCollective("MPI_Ialltoallw");
+
+  // Nonblocking barrier.
+  AddNonBlockingCollective("MPI_Ibarrier");
+
+  // Prefix reductions (scans).
+  AddCollective("MPI_Scan");
+  AddNonBlockingCollective("MPI_Iscan");
+  AddCollective("MPI_Exscan");
+  AddNonBlockingCollective("MPI_Iexscan");
+
+  // Reduce-scatter family.
+  AddCollective("MPI_Reduce_scatter");
+  AddNonBlockingCollective("MPI_Ireduce_scatter");
+  AddCollective("MPI_Reduce_scatter_block");
+  AddNonBlockingCollective("MPI_Ireduce_scatter_block");
 }
 
 void MPIFunctionClassifier::initAdditionalIdentifiers(ASTContext &ASTCtx) {
diff --git a/clang/test/Analysis/MPIMock.h b/clang/test/Analysis/MPIMock.h
index 01d2d42fc58a3..f3bc0676d132b 100644
--- a/clang/test/Analysis/MPIMock.h
+++ b/clang/test/Analysis/MPIMock.h
@@ -37,6 +37,7 @@ namespace std { template<class T> struct complex { T real; T imag; }; }
 #define MPI_COMM_WORLD 0
 #define MPI_STATUS_IGNORE 0
 #define MPI_STATUSES_IGNORE 0
+#define MPI_REQUEST_NULL 0
 #define MPI_SUM 0
 
 // mock functions
@@ -53,3 +54,9 @@ int MPI_Reduce(const void *, void *, int, MPI_Datatype, MPI_Op, int, MPI_Comm);
 int MPI_Ireduce(const void *, void *, int, MPI_Datatype, MPI_Op, int, MPI_Comm,
     MPI_Request *);
 int MPI_Bcast(void *, int count, MPI_Datatype, int, MPI_Comm);
+int MPI_Iscatterv(const void *, const int[], const int[], MPI_Datatype, void *,
+    int, MPI_Datatype, int, MPI_Comm, MPI_Request *);
+int MPI_Iscan(const void *, void *, int, MPI_Datatype, MPI_Op, MPI_Comm,
+    MPI_Request *);
+int MPI_Ireduce_scatter(const void *, void *, const int[], MPI_Datatype,
+    MPI_Op, MPI_Comm, MPI_Request *);
diff --git a/clang/test/Analysis/mpichecker.cpp b/clang/test/Analysis/mpichecker.cpp
index f7644520ebcf5..f2fd6c0951a2f 100644
--- a/clang/test/Analysis/mpichecker.cpp
+++ b/clang/test/Analysis/mpichecker.cpp
@@ -48,6 +48,46 @@ void matchedWait3() {
   }
 } // no error
 
+void matchedWaitIscatterv() {
+  double buf = 0;
+  MPI_Request req = MPI_REQUEST_NULL;
+  MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+                0, MPI_COMM_WORLD, &req);
+  MPI_Wait(&req, MPI_STATUS_IGNORE);
+} // no error: this used to report a FP before GH208107.
+
+void missingWaitIscatterv() {
+  double buf = 0;
+  MPI_Request req = MPI_REQUEST_NULL;
+  MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+                0, MPI_COMM_WORLD, &req);
+} // expected-warning {{Request 'req' has no matching wait.}}
+
+void doubleNonblockingIscatterv() {
+  double buf = 0;
+  MPI_Request req = MPI_REQUEST_NULL;
+  MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+                0, MPI_COMM_WORLD, &req);
+  // expected-warning@+1 {{Double nonblocking on request 'req'.}}
+  MPI_Iscatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, &buf, 0, MPI_DATATYPE_NULL,
+                0, MPI_COMM_WORLD, &req);
+  MPI_Wait(&req, MPI_STATUS_IGNORE);
+}
+
+void matchedWaitIscan() {
+  double buf = 0;
+  MPI_Request req = MPI_REQUEST_NULL;
+  MPI_Iscan(MPI_IN_PLACE, &buf, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD, &req);
+  MPI_Wait(&req, MPI_STATUS_IGNORE);
+} // no error
+
+void missingWaitIreduceScatter() {
+  double buf = 0;
+  MPI_Request req = MPI_REQUEST_NULL;
+  MPI_Ireduce_scatter(MPI_IN_PLACE, &buf, NULL, MPI_DOUBLE, MPI_SUM,
+                      MPI_COMM_WORLD, &req);
+} // expected-warning {{Request 'req' has no matching wait.}}
+
 void missingWait1() { // Check missing wait for dead region.
   double buf = 0;
   MPI_Request sendReq1;

@steakhal

Copy link
Copy Markdown
Contributor Author

This is practically vibe-coded and I don't really want to get myself into MPI.
I'll rely on @SuperSamus to judge if what it does is actually good.
This looks plausible to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:static analyzer clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Analyzer] MPI-Checker: doesn't classify some functions, causing false positives

1 participant