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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions