Skip to content

Commit 65f9786

Browse files
fix: unify residual-support cap as kMaxResidualSupport=6 (#31)
* fix: unify residual-support cap as kMaxResidualSupport=6 The residual-family solvers (ghost basis, factored ghost, residual polynomial recovery, ExtractPolyCore) cap support size at 6 to keep SolveGhostResidual's combo/args/var_indices buffers and ProbePoint::values within fixed bounds. The constant 6 was duplicated across at least seven sites — five residual passes, IsBooleanNullResidual, ExtractPolyCore, plus the array sizes themselves. The companion GhostPrimitive::arity contract — that each primitive's eval/build span is exactly the declared arity — was unenforced on either side, so a future basis entry with arity > 6 would silently never run today (skipped by support-size guard) but corrupt stack memory if a caller ever passed a smaller cap. Closes the ghost-residual-arity cluster from the 2026-05-04 audit: - decomposition-engine-cross-4: max_real_vars=6 cap enforced inconsistently - decomposition-engine-cross-1: GhostPrimitive arity vs fixed 6-element buffers - decomposition-engine-cross-3: GhostPrimitive Eval/Build span-size contract implicit Adds `inline constexpr uint32_t kMaxResidualSupport = 6;` in GhostResidualSolver.h and references it from every site: - 4 residual-pass guards in DecompositionPasses.cpp - ExtractPolyCore in DecompositionEngine.cpp - IsBooleanNullResidual support-size check - ProbePoint::values, combo, args, var_indices, ghost_args array sizes - the `prim.arity > kSupportSize` filters in SolveGhostResidual / SolveFactoredGhostResidual now also reject `prim.arity > kMaxResidualSupport` Hardens GhostBasis.cpp with named arity constants (kMulSubAndArity, kMul3SubAnd3Arity), a static_assert that they fit in kMaxResidualSupport, and assert(args.size() == arity) at every eval/build entry. A future primitive that declares an arity > 6 either fails to compile (static_assert) or fails loud at runtime if a caller ever passes a mis-sized span. * style: clang-format GhostBasis static_assert block
1 parent 9bc5e70 commit 65f9786

5 files changed

Lines changed: 49 additions & 21 deletions

File tree

include/cobra/core/GhostResidualSolver.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99

1010
namespace cobra {
1111

12+
// Maximum support size for residual-family solvers (ghost basis,
13+
// factored ghost, residual polynomial recovery, ExtractPolyCore).
14+
// Sets the array sizes inside SolveGhostResidual (combo, args,
15+
// var_indices, ProbePoint::values) AND the runtime cap enforced
16+
// by every residual pass / IsBooleanNullResidual. Changing this
17+
// constant requires audit of every site that previously hardcoded 6.
18+
inline constexpr uint32_t kMaxResidualSupport = 6;
19+
1220
struct GhostSolveResult
1321
{
1422
std::unique_ptr< Expr > expr;
@@ -28,7 +36,7 @@ namespace cobra {
2836

2937
// Attempts to solve a boolean-null residual as a constant-coefficient
3038
// single ghost primitive. Returns expression in original variable space.
31-
// Requires support.size() <= 6.
39+
// Requires support.size() <= kMaxResidualSupport.
3240
SolverResult< GhostSolveResult > SolveGhostResidual(
3341
const Evaluator &residual_eval, const std::vector< uint32_t > &support,
3442
uint32_t num_vars, uint32_t bitwidth
@@ -38,7 +46,7 @@ namespace cobra {
3846
// where q is a constant polynomial and g is a ghost primitive.
3947
// Uses RecoverWeightedPoly with max_degree=0, grid_degree=2.
4048
// Enumerates ghost primitives in priority order (mul_sub_and first).
41-
// Requires support.size() <= 6.
49+
// Requires support.size() <= kMaxResidualSupport.
4250
SolverResult< GhostSolveResult > SolveFactoredGhostResidual(
4351
const Evaluator &residual_eval, const std::vector< uint32_t > &support,
4452
uint32_t num_vars, uint32_t bitwidth, uint8_t max_degree = 0, uint8_t grid_degree = 2

lib/core/DecompositionEngine.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "cobra/core/AuxVarEliminator.h"
44
#include "cobra/core/BitWidth.h"
55
#include "cobra/core/ExprUtils.h"
6+
#include "cobra/core/GhostResidualSolver.h"
67
#include "cobra/core/MultivarPolyRecovery.h"
78
#include "cobra/core/PassContract.h"
89
#include "cobra/core/PolyExprBuilder.h"
@@ -220,7 +221,7 @@ namespace cobra {
220221
auto fw_elim = EliminateAuxVars(ctx.sig, ctx.vars, ctx.opts.evaluator, kBw);
221222
const auto kRealCount = static_cast< uint32_t >(fw_elim.real_vars.size());
222223

223-
if (kRealCount > 6) {
224+
if (kRealCount > kMaxResidualSupport) {
224225
return SolverResult< CoreCandidate >::Inapplicable(
225226
ReasonDetail{
226227
.top = { .code = { ReasonCategory::kGuardFailed,

lib/core/DecompositionPasses.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ namespace cobra {
466466
}
467467
const auto res_real_count =
468468
static_cast< uint32_t >(residual.remainder_elim.real_vars.size());
469-
if (res_real_count > 6) {
469+
if (res_real_count > kMaxResidualSupport) {
470470
return Ok(
471471
PassResult{
472472
.decision = PassDecision::kNotApplicable,
@@ -525,7 +525,7 @@ namespace cobra {
525525
}
526526
const auto res_real_count =
527527
static_cast< uint32_t >(residual.remainder_elim.real_vars.size());
528-
if (res_real_count > 6) {
528+
if (res_real_count > kMaxResidualSupport) {
529529
return Ok(
530530
PassResult{
531531
.decision = PassDecision::kNotApplicable,
@@ -584,7 +584,7 @@ namespace cobra {
584584
}
585585
const auto res_real_count =
586586
static_cast< uint32_t >(residual.remainder_elim.real_vars.size());
587-
if (res_real_count > 6) {
587+
if (res_real_count > kMaxResidualSupport) {
588588
return Ok(
589589
PassResult{
590590
.decision = PassDecision::kNotApplicable,
@@ -642,7 +642,7 @@ namespace cobra {
642642
const auto &residual = std::get< RemainderStatePayload >(item.payload);
643643
const auto res_real_count =
644644
static_cast< uint32_t >(residual.remainder_elim.real_vars.size());
645-
if (res_real_count > 6) {
645+
if (res_real_count > kMaxResidualSupport) {
646646
return Ok(PassResult{ .decision = PassDecision::kNotApplicable });
647647
}
648648

lib/core/GhostBasis.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
#include "cobra/core/GhostBasis.h"
22
#include "cobra/core/BitWidth.h"
3+
#include "cobra/core/GhostResidualSolver.h"
4+
#include <cassert>
35

46
namespace cobra {
57

68
namespace {
79

10+
// Each eval/build pair takes a span sized exactly `arity`.
11+
// Asserting at entry pins the producer/consumer contract: a
12+
// future caller that passes a too-small span fails loud instead
13+
// of reading past the underlying buffer.
14+
constexpr uint8_t kMulSubAndArity = 2;
15+
constexpr uint8_t kMul3SubAnd3Arity = 3;
16+
17+
static_assert(
18+
kMulSubAndArity <= kMaxResidualSupport && kMul3SubAnd3Arity <= kMaxResidualSupport,
19+
"Ghost primitive arity exceeds kMaxResidualSupport — "
20+
"consumer buffers (combo, args, var_indices) would overflow"
21+
);
22+
823
// mul_sub_and: x*y - (x&y)
924
uint64_t EvalMulSubAnd(std::span< const uint64_t > args, uint32_t bw) {
25+
assert(args.size() == kMulSubAndArity);
1026
const uint64_t kMask = Bitmask(bw);
1127
return ((args[0] * args[1]) - (args[0] & args[1])) & kMask;
1228
}
1329

1430
std::unique_ptr< Expr > BuildMulSubAnd(std::span< const uint32_t > vars) {
31+
assert(vars.size() == kMulSubAndArity);
1532
return Expr::Add(
1633
Expr::Mul(Expr::Variable(vars[0]), Expr::Variable(vars[1])),
1734
Expr::Negate(Expr::BitwiseAnd(Expr::Variable(vars[0]), Expr::Variable(vars[1])))
@@ -20,11 +37,13 @@ namespace cobra {
2037

2138
// mul3_sub_and3: x*y*z - (x&y&z)
2239
uint64_t EvalMul3SubAnd3(std::span< const uint64_t > args, uint32_t bw) {
40+
assert(args.size() == kMul3SubAnd3Arity);
2341
const uint64_t kMask = Bitmask(bw);
2442
return ((args[0] * args[1] * args[2]) - (args[0] & args[1] & args[2])) & kMask;
2543
}
2644

2745
std::unique_ptr< Expr > BuildMul3SubAnd3(std::span< const uint32_t > vars) {
46+
assert(vars.size() == kMul3SubAnd3Arity);
2847
return Expr::Add(
2948
Expr::Mul(
3049
Expr::Mul(Expr::Variable(vars[0]), Expr::Variable(vars[1])),
@@ -44,12 +63,12 @@ namespace cobra {
4463
const std::vector< GhostPrimitive > &GetGhostBasis() {
4564
static const std::vector< GhostPrimitive > kBasis = {
4665
{ .name = "mul_sub_and",
47-
.arity = 2,
66+
.arity = kMulSubAndArity,
4867
.symmetric = true,
4968
.eval = EvalMulSubAnd,
5069
.build = BuildMulSubAnd },
5170
{ .name = "mul3_sub_and3",
52-
.arity = 3,
71+
.arity = kMul3SubAnd3Arity,
5372
.symmetric = true,
5473
.eval = EvalMul3SubAnd3,
5574
.build = BuildMul3SubAnd3 },

lib/core/GhostResidualSolver.cpp

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ namespace cobra {
2828
// Mixed parity gives varied 2-adic valuations.
2929
struct ProbePoint
3030
{
31-
std::array< uint64_t, 6 > values{}; // max 6 vars
31+
std::array< uint64_t, kMaxResidualSupport > values{};
3232
};
3333

3434
constexpr int kNumProbes = 8;
@@ -62,8 +62,8 @@ namespace cobra {
6262
9, 12, 17, 20, 25, 30, 35, 40, 47, 56, 63, 70, 79, 86, 95, 102,
6363
};
6464
for (size_t p = 0; p < kNumProbes; ++p) {
65-
for (uint32_t v = 0; v < num_vars && v < 6; ++v) {
66-
bank[p].values[v] = kSeeds[(p * 6) + v] & kMask;
65+
for (uint32_t v = 0; v < num_vars && v < kMaxResidualSupport; ++v) {
66+
bank[p].values[v] = kSeeds[(p * kMaxResidualSupport) + v] & kMask;
6767
}
6868
}
6969
}
@@ -85,7 +85,7 @@ namespace cobra {
8585
// Condition 2: nonzero at some non-boolean full-width point
8686
// Probe in support-local space, map to full space via support[].
8787
const auto kSupportSize = static_cast< uint32_t >(support.size());
88-
if (kSupportSize > 6) { return false; }
88+
if (kSupportSize > kMaxResidualSupport) { return false; }
8989

9090
std::array< ProbePoint, kNumProbes > bank{};
9191
GenerateProbeBank(bank, kSupportSize, bitwidth);
@@ -126,17 +126,17 @@ namespace cobra {
126126
}
127127

128128
for (const auto &prim : basis) {
129-
if (prim.arity > kSupportSize) { continue; }
129+
if (prim.arity > kSupportSize || prim.arity > kMaxResidualSupport) { continue; }
130130

131131
// Enumerate strictly increasing index combinations
132-
std::array< uint32_t, 6 > combo{};
132+
std::array< uint32_t, kMaxResidualSupport > combo{};
133133
for (uint8_t i = 0; i < prim.arity; ++i) { combo[i] = i; }
134134
auto combo_span = std::span< uint32_t >{ combo.data(), prim.arity };
135135

136136
do {
137137
// Evaluate ghost at each probe point
138138
std::array< uint64_t, kNumProbes > g_vals{};
139-
std::array< uint64_t, 6 > args{};
139+
std::array< uint64_t, kMaxResidualSupport > args{};
140140
for (int p = 0; p < kNumProbes; ++p) {
141141
for (uint8_t a = 0; a < prim.arity; ++a) {
142142
args[a] = bank[static_cast< size_t >(p)].values[combo[a]];
@@ -189,7 +189,7 @@ namespace cobra {
189189
if (!cross_ok) { continue; }
190190

191191
// Build the expression: c * ghost(var_indices)
192-
std::array< uint32_t, 6 > var_indices{};
192+
std::array< uint32_t, kMaxResidualSupport > var_indices{};
193193
for (uint8_t a = 0; a < prim.arity; ++a) { var_indices[a] = support[combo[a]]; }
194194
auto ghost_expr =
195195
prim.build(std::span< const uint32_t >{ var_indices.data(), prim.arity });
@@ -232,22 +232,22 @@ namespace cobra {
232232
const auto &basis = GetGhostBasis();
233233

234234
for (const auto &prim : basis) {
235-
if (prim.arity > kSupportSize) { continue; }
235+
if (prim.arity > kSupportSize || prim.arity > kMaxResidualSupport) { continue; }
236236

237237
// Enumerate strictly increasing index combinations
238-
std::array< uint32_t, 6 > combo{};
238+
std::array< uint32_t, kMaxResidualSupport > combo{};
239239
for (uint8_t i = 0; i < prim.arity; ++i) { combo[i] = i; }
240240
auto combo_span = std::span< uint32_t >{ combo.data(), prim.arity };
241241

242242
do {
243243
// Map support-local combo to original-space variable indices
244-
std::array< uint32_t, 6 > var_indices{};
244+
std::array< uint32_t, kMaxResidualSupport > var_indices{};
245245
for (uint8_t a = 0; a < prim.arity; ++a) { var_indices[a] = support[combo[a]]; }
246246

247247
// Build weight function from ghost primitive + tuple
248248
WeightFn weight =
249249
[&prim, combo](std::span< const uint64_t > args, uint32_t bw) -> uint64_t {
250-
std::array< uint64_t, 6 > ghost_args{};
250+
std::array< uint64_t, kMaxResidualSupport > ghost_args{};
251251
for (uint8_t a = 0; a < prim.arity; ++a) { ghost_args[a] = args[combo[a]]; }
252252
return prim.eval(
253253
std::span< const uint64_t >{ ghost_args.data(), prim.arity }, bw

0 commit comments

Comments
 (0)