Symptom
Any GLM aggregate whose IRLS inner loop runs a weighted-least-squares QR (poisson_fit_agg, negbinom_fit_agg, tweedie_fit_agg, … — anything reaching faer's ColPivQr) segfaults deterministically when:
- the fit has ≥ ~150 observations (crash threshold bisected to between n=100 and n=150), AND
- the query runs inside the DuckDB v1.4.4 sqllogictest harness (
build/release/test/unittest).
It does not reproduce:
- in the DuckDB CLI shell with identical data (default threads or
threads=1),
- on DuckDB v1.5.3 (shell or harness),
- calling
fit_negbinomial / fit_poisson directly from Rust (including on 256KB-stack threads).
Inside the harness it crashes with threads=1 too, so it is not a worker-thread race.
Backtrace
#0 private_gemm_x86::pack_lhs () ← SIGSEGV, si_addr = 0x0 (NULL deref)
#1 private_gemm_x86::millikernel_colmajor ()
#2 private_gemm_x86::gemm ()
#3 faer::linalg::matmul::triangular::mat_x_mat_into_lower_impl_unchecked ()
#6 faer::linalg::householder::upgrade_householder_factor ()
#7 faer::linalg::solvers::ColPivQr<T>::new_imp ()
#8 anofox_regression::solvers::negative_binomial::NegativeBinomialRegressor::solve_weighted_ls ()
#9 NegativeBinomialRegressor::fit_irls ()
#10 anofox_negbinomial_fit ()
#11 duckdb::NegBinomAggFinalize(...)
#12 duckdb::RowOperations::FinalizeStates(...)
Faulting instruction is a mulsd (%r14,%r12,1),%xmm0 inside the packing loop with a NULL base pointer.
Size threshold
faer's QR uses an unblocked path for small matrices and switches to blocked Householder (which calls the gemm microkernels) for larger ones — only the blocked path crashes. That's why every pre-existing GLM test stayed green: all fixtures were < 150 rows.
| n rows |
harness v1.4.4 |
| 20 / 50 / 100 |
✅ pass |
| 150 / 200 |
💥 SIGSEGV (3/3 runs) |
Ruled out
- My new aggregate C++ (structural clone of
poisson_aggregate.cpp; poisson_fit_agg itself crashes identically on the same fixture).
- FFI struct layout mismatch (C header vs
#[repr(C)] verified field-by-field).
- Stack exhaustion (fault address is NULL, not a guard page; direct Rust runs pass on 256KB stacks).
- Worker-thread placement (crashes with
SET threads=1 in the harness; crashed on main thread in one gdb run).
- Harness client settings (
enable_progress_bar, PRAGMA enable_verification in the shell don't reproduce it).
Open leads
- The harness
require → LOAD path dlopens the loadable .duckdb_extension while the statically linked copy is also present in libduckdb.so — two copies of faer/its dispatch state in one process. The shell auto-loads only the static copy. Explicit LOAD in the shell did not reproduce, but the harness load order/path differs (local repository, gzipped artifact) and was not fully replicated.
- v1.4.4 vs v1.5.3
extension-ci-tools differ in extension symbol visibility/linking, which would change whether the two copies' dynamic symbols interpose.
Workaround in place
All GLM test fixtures are capped at 100 rows (commits on the #88–#92 branches reference this issue). Production usage is unaffected — the crash needs the v1.4.4 harness specifically.
Repro recipe
git submodule update --init duckdb extension-ci-tools posthog-telemetry
(cd duckdb && git fetch --depth 1 origin tag v1.4.4 && git checkout v1.4.4)
(cd extension-ci-tools && git fetch --depth 1 origin v1.4.4 && git checkout FETCH_HEAD)
GEN=ninja make release
# any GLM agg with a 200-row fixture:
./build/release/test/unittest "test/sql/regression/test_negbinom_basic.test" # with range(0,200)
🤖 Generated with Claude Code
Symptom
Any GLM aggregate whose IRLS inner loop runs a weighted-least-squares QR (
poisson_fit_agg,negbinom_fit_agg,tweedie_fit_agg, … — anything reachingfaer's ColPivQr) segfaults deterministically when:build/release/test/unittest).It does not reproduce:
threads=1),fit_negbinomial/fit_poissondirectly from Rust (including on 256KB-stack threads).Inside the harness it crashes with
threads=1too, so it is not a worker-thread race.Backtrace
Faulting instruction is a
mulsd (%r14,%r12,1),%xmm0inside the packing loop with a NULL base pointer.Size threshold
faer's QR uses an unblocked path for small matrices and switches to blocked Householder (which calls the gemm microkernels) for larger ones — only the blocked path crashes. That's why every pre-existing GLM test stayed green: all fixtures were < 150 rows.
Ruled out
poisson_aggregate.cpp;poisson_fit_aggitself crashes identically on the same fixture).#[repr(C)]verified field-by-field).SET threads=1in the harness; crashed on main thread in one gdb run).enable_progress_bar,PRAGMA enable_verificationin the shell don't reproduce it).Open leads
require→LOADpath dlopens the loadable.duckdb_extensionwhile the statically linked copy is also present inlibduckdb.so— two copies of faer/its dispatch state in one process. The shell auto-loads only the static copy. ExplicitLOADin the shell did not reproduce, but the harness load order/path differs (local repository, gzipped artifact) and was not fully replicated.extension-ci-toolsdiffer in extension symbol visibility/linking, which would change whether the two copies' dynamic symbols interpose.Workaround in place
All GLM test fixtures are capped at 100 rows (commits on the #88–#92 branches reference this issue). Production usage is unaffected — the crash needs the v1.4.4 harness specifically.
Repro recipe
🤖 Generated with Claude Code