Skip to content

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

@Sunrisepeak

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) * 2 crashed 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 above
  • template<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 the Vec variants
  • template<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 every x * y in every importer crash, for unrelated types (Point*2, Mat*2.0, softdouble*softdouble, Size*2 all crashed in opencv-m); operator+ etc. stay fine.
  • Function body is irrelevant (trivial-body variant still crashes).
  • Two crash flavors, same root: parse-time AddArgumentDependentLookupCandidates segv, and (via gtest's printer machinery instantiating around such names) unbounded recursive InstantiateFunctionDefinition → 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

  1. 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").
  2. 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.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions