Repository navigation
clang 20/22 module BMI: replacement-operator templates with params not pinned by arg1 poison name lookup for all importers (frontend SIGSEGV; gcc + clang18 fine) #256
Description
Summary
Certain replacement-operator template shapes, when compiled into a C++23 module BMI by clang, "poison" the lookup of that operator/function name for every importer: any TU that imports the module and uses that name on any type crashes the clang frontend (SIGSEGV, exit 139). gcc 16.1.0 is completely unaffected.
This bites the mcpp module-package pattern directly: a module layer that mirrors an upstream header's static inline operator templates (identical signature + trivially-true constraint, the standard mixed-TU subsumption recipe) will hit it as soon as the upstream shape has template parameters that are not all pinned by the first function argument.
- Affected: mcpp llvm toolchains 20.1.7 and 22.1.8 (both crash identically)
- Not affected: gcc 16.1.0; system clang 18.1.3 compiles the same repro fine → regression introduced between clang 18 and 20
- Found in: opencv-m single-repo work (
feat/vendor-single-repo) — the opencv-m gtest suite had never been built with any clang before (macOS/windows CI only build small consumer smokes), so e.g.cv::Point(1,2) * 2crashed the importer on every clang platform without CI ever noticing.
Standalone repro (40 lines, no OpenCV)
matx.h — mimics an upstream header (TU-local static inline operator templates):
#pragma once
namespace cv {
template<typename T, int m, int n> struct Matx {
T val[m*n];
typedef Matx<T, m, n> mat_type;
enum { rows = m, cols = n };
};
template<typename T> struct Point_ { T x, y; };
using Point = Point_<int>;
template<typename T, int m, int n, int l> static inline
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b) { return Matx<T,m,n>{}; }
template<typename T> static inline
Point_<T> operator*(const Point_<T>& a, int b) { return Point_<T>{a.x*b, a.y*b}; }
}m.cppm — the module layer (upstream shape + trivially-true constraint, i.e. the subsumption recipe):
module;
#include "matx.h"
export module m;
export namespace cv {
using cv::Matx;
using cv::Point_;
using cv::Point;
inline namespace repl {
template<typename T> inline constexpr bool pick = true;
template<typename T, int m, int n, int l> requires pick<T> inline
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b) { return Matx<T,m,n>{}; }
template<typename T> requires pick<T> inline
Point_<T> operator*(const Point_<T>& a, int b) { return Point_<T>{a.x*b, a.y*b}; }
}
}use.cpp — note it only multiplies a Point, never a Matx:
import m;
int main() { cv::Point p{1,2}; auto q = p * 2; return q.x; }$ clang++ -std=c++23 --precompile m.cppm -o m.pcm # fine
$ clang++ -std=c++23 -fprebuilt-module-path=. -c use.cpp
PLEASE submit a bug report ... Stack dump:
#4 clang::Sema::AddArgumentDependentLookupCandidates(...)
#5 clang::Sema::CreateOverloadedBinOp(...)
Control matrix (this exact repro):
| compiler | with the 4-param Matx*Matx template |
without it |
|---|---|---|
| clang 20.1.7 (mcpp toolchain) | SIGSEGV | ok |
| clang 22.1.8 (mcpp toolchain) | SIGSEGV | ok |
| clang 18.1.3 (Ubuntu) | ok | ok |
| gcc 16.1.0 | ok | ok |
Trigger taxonomy (bisected in opencv-m with a --precompile harness, per-template-block subsets)
Poisonous — template parameters not all pinned by the first argument (or requiring a repeat-consistency check):
template<typename T, int m, int n, int l> op*(Matx<T,m,l>, Matx<T,l,n>)← the repro abovetemplate<typename T, int m> determinant(const Matx<T,m,m>&)(NTTP repeated inside one parameter type)template<typename T1, typename T2, int n> op+=(Matx<T1,..>&, const Matx<T2,..>&)(second typename only in arg 2) — same for-=, and for theVecvariantstemplate<typename T, typename T2, int m, int n> op<<(const Matx<T,m,n>&, T2)(comma-initializer)
NOT poisonous (all verified): same-shape ops where every parameter is pinned by arg 1 — op+(Matx<T,m,n>, Matx<T,m,n>), op*(Matx<T,m,n>, int/float/double), op*(Matx<T,m,n>, Vec<T,n>), (Matx<T,m,n>, Matx<T,n,m>), plain norm/trace(Matx<T,m,n>).
Properties:
- The crash is name-keyed: one poisoned
operator*decl makes everyx * yin every importer crash, for unrelated types (Point*2,Mat*2.0,softdouble*softdouble,Size*2all crashed in opencv-m);operator+etc. stay fine. - Function body is irrelevant (trivial-body variant still crashes).
- Two crash flavors, same root: parse-time
AddArgumentDependentLookupCandidatessegv, and (via gtest's printer machinery instantiating around such names) unbounded recursiveInstantiateFunctionDefinition→ stack overflow.
Workaround that ships (opencv-m commit 42aeb20)
Reformulate over whole deduced operand types — call-compatible, and mixed-TU semantics survive because the upstream exact-pattern static-inline is more specialized and wins there:
template<typename MA, typename MB>
requires pick<typename MA::value_type>
&& __is_same(MA, typename MA::mat_type) // "is a Matx"
&& __is_same(MB, Matx<typename MB::value_type, (int)MA::rows, (int)MA::cols>)
inline MA& operator += (MA& a, const MB& b);With every poisoned shape rewritten this way, opencv-m is green on llvm 20.1.7 and 22.1.8 (6/6 suites), and gcc 16.1.0 regression stays green.
Asks
- Track this as a known clang-toolchain hazard for module packages (docs note in the module-package guidance would save others the bisect: "avoid operator templates whose parameters aren't all pinned by the first argument; deduce whole operand types instead").
- Upstream LLVM report: I can file it with the standalone repro above (clang 18 ok / 20+22 crash gives a clean regression window). Happy to link it back here.
- Possibly related for the toolchain matrix: consider adding a canary to mcpp's llvm-toolchain e2e that imports a module exporting such a shape, so a future clang bump that fixes (or re-breaks) this is visible.
Repro artifacts: standalone 3-file repro (above, self-contained); full-context preprocessed sources + run scripts archived from opencv-m (clang++ -v crash dumps for both 20.1.7 and 22.1.8) — available on request.
Environment: mcpp 0.0.101, linux x86_64 (Ubuntu 24.04, kernel 6.8), toolchains from mcpp toolchain install (llvm 20.1.7 / 22.1.8, gcc 16.1.0).
Activity
batch: #261 windows cmdline, #257 clang depfile, #258 conditional per…
Sunrisepeak commentedon Jul 21, 2026
The mcpp-side asks (1 and 3) shipped in 0.0.102 (PR #264, 5807e36):
tests/e2e/150_clang_module_operator_template.shis a state canary over the bundled LLVMs: crash recorded as expected for 20–22, ok for 18–19, and the test fails when reality stops matching. A future Clang bump that fixes this is as much a signal as one that re-breaks it. Reproduced locally across the full matrix (clang 20.1.7 crash, 22.1.8 crash, system clang 18.1.3 ok, gcc 16.1.0 ok).Keeping this open to track ask 2 — the upstream LLVM report — since the actual defect is in the Clang frontend, not in mcpp. Will link the LLVM issue here once filed.