Skip to content

Commit f4071dd

Browse files
TransurgeonTransurgeonclaude
authored
Size the gathered Jacobian by the true selected-row nnz sum (#105)
sparse_index_alloc capped the output capacity at the source's total nnz, a bound that only holds for distinct (selection/permutation) indices. A gather with enough DUPLICATED indices selects more entries than the source has (e.g. x[[0,0,1,2,2,3]] on a size-4 variable: 6 > 4), and the fill loop wrote the excess past the end of the buffers, corrupting the heap (cvxpy#3442 — DNLP segfault during derivative-structure init). Sum the selected rows' nnz for an exact allocation. The sum replaces the sat_mul_int dense bound; a checked add errors out before wrapping if the true count is not CSR-representable. New tests (verified to hit heap-buffer-overflow under ASan without the fix): the gather Jacobian itself, and the jacobian + weighted-sum Hessian of multiply(a[idx], b[idx]) mirroring the issue. Co-authored-by: Transurgeon <peter.zijie@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 0a3070d commit f4071dd

5 files changed

Lines changed: 114 additions & 2 deletions

File tree

src/utils/sparse_matrix.c

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "utils/mini_numpy.h"
2525
#include "utils/tracked_alloc.h"
2626
#include "utils/utils.h"
27+
#include <stdio.h>
2728
#include <stdlib.h>
2829
#include <string.h>
2930

@@ -124,8 +125,24 @@ static void sparse_transpose_fill_values(const matrix *self, matrix *out)
124125
static matrix *sparse_index_alloc(matrix *self, const int *indices, int n_idxs)
125126
{
126127
CSR_matrix *Jx = ((sparse_matrix *) self)->csr;
127-
int alloc_ub = sat_mul_int(n_idxs, self->n);
128-
CSR_matrix *J = new_CSR_matrix(n_idxs, self->n, MIN(Jx->nnz, alloc_ub));
128+
129+
/* Exact output nnz: sum the selected rows' nnz. Jx->nnz is NOT an upper
130+
bound — duplicated indices select the same source row more than once
131+
(cvxpy#3442). Duplicated gathers of dense rows can push the true count
132+
past INT_MAX, which a CSR cannot represent, so fail before wrapping. */
133+
int nnz = 0;
134+
for (int i = 0; i < n_idxs; i++)
135+
{
136+
int len = Jx->p[indices[i] + 1] - Jx->p[indices[i]];
137+
if (len > INT_MAX - nnz)
138+
{
139+
fprintf(stderr, "Error in sparse_index_alloc: gathered nnz "
140+
"exceeds INT_MAX.\n");
141+
exit(1);
142+
}
143+
nnz += len;
144+
}
145+
CSR_matrix *J = new_CSR_matrix(n_idxs, self->n, nnz);
129146

130147
J->p[0] = 0;
131148
for (int i = 0; i < n_idxs; i++)

tests/all_tests.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ int main(void)
194194
mu_run_test(test_jacobian_elementwise_mult_2, tests_run);
195195
mu_run_test(test_jacobian_elementwise_mult_3, tests_run);
196196
mu_run_test(test_jacobian_elementwise_mult_4, tests_run);
197+
mu_run_test(test_jacobian_elementwise_mult_duplicate_gathers, tests_run);
197198
mu_run_test(test_quad_over_lin1, tests_run);
198199
mu_run_test(test_quad_over_lin2, tests_run);
199200
mu_run_test(test_quad_over_lin3, tests_run);
@@ -223,6 +224,7 @@ int main(void)
223224
mu_run_test(test_index_jacobian_of_variable, tests_run);
224225
mu_run_test(test_index_jacobian_of_log, tests_run);
225226
mu_run_test(test_index_jacobian_repeated, tests_run);
227+
mu_run_test(test_index_jacobian_duplicates_exceed_source_nnz, tests_run);
226228
mu_run_test(test_sum_of_index, tests_run);
227229
mu_run_test(test_promote_scalar_jacobian, tests_run);
228230
mu_run_test(test_promote_scalar_to_matrix_jacobian, tests_run);
@@ -307,6 +309,7 @@ int main(void)
307309
mu_run_test(test_wsum_hess_multiply_sparse_random, tests_run);
308310
mu_run_test(test_wsum_hess_multiply_1, tests_run);
309311
mu_run_test(test_wsum_hess_multiply_2, tests_run);
312+
mu_run_test(test_wsum_hess_multiply_duplicate_gathers, tests_run);
310313
mu_run_test(test_wsum_hess_left_matmul, tests_run);
311314
mu_run_test(test_wsum_hess_left_matmul_matrix, tests_run);
312315
mu_run_test(test_wsum_hess_left_matmul_exp_composite, tests_run);

tests/jacobian_tests/affine/test_index.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,32 @@ const char *test_index_jacobian_repeated(void)
116116
return 0;
117117
}
118118

119+
const char *test_index_jacobian_duplicates_exceed_source_nnz(void)
120+
{
121+
/* x[[0,0,1,2,2,3]] on a size-4 variable: the gathered Jacobian has 6
122+
* nonzeros, MORE than the source identity's 4. Pins the allocator sizing
123+
* the output by the true per-row sum instead of the source nnz
124+
* (cvxpy#3442 — undersized buffers overflowed the heap on the extra rows). */
125+
double u[4] = {1.0, 2.0, 3.0, 4.0};
126+
int indices[6] = {0, 0, 1, 2, 2, 3};
127+
expr *var = new_variable(4, 1, 0, 4);
128+
expr *idx = new_index(var, 1, 6, indices, 6);
129+
idx->forward(idx, u);
130+
jacobian_init(idx);
131+
idx->eval_jacobian(idx);
132+
133+
double expected_x[6] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0};
134+
int expected_p[7] = {0, 1, 2, 3, 4, 5, 6};
135+
int expected_i[6] = {0, 0, 1, 2, 2, 3};
136+
137+
mu_assert("vals fail", cmp_values(idx->jacobian, expected_x, 6));
138+
mu_assert("sparsity fail",
139+
cmp_sparsity(idx->jacobian, expected_p, expected_i, 6, 6));
140+
141+
free_expr(idx);
142+
return 0;
143+
}
144+
119145
const char *test_sum_of_index(void)
120146
{
121147
/* sum(x[0, 2]) = x[0] + x[2]

tests/jacobian_tests/bivariate_full_dom/test_elementwise_mult.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,38 @@ const char *test_jacobian_elementwise_mult_3(void)
118118
return 0;
119119
}
120120

121+
const char *test_jacobian_elementwise_mult_duplicate_gathers(void)
122+
{
123+
// var = (a, b) where a is 4 x 1 at offset 0 and b is 4 x 1 at offset 4.
124+
// We compute the jacobian of a[idx] * b[idx] with DUPLICATED gather
125+
// indices idx = [0, 0, 1, 2, 2, 3] (cvxpy#3442): each gather Jacobian has
126+
// 6 nnz, more than its source variable's 4.
127+
double u_vals[8] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};
128+
int indices[6] = {0, 0, 1, 2, 2, 3};
129+
expr *a = new_variable(4, 1, 0, 8);
130+
expr *b = new_variable(4, 1, 4, 8);
131+
expr *ga = new_index(a, 1, 6, indices, 6);
132+
expr *gb = new_index(b, 1, 6, indices, 6);
133+
expr *node = new_elementwise_mult(ga, gb);
134+
135+
node->forward(node, u_vals);
136+
jacobian_init(node);
137+
node->eval_jacobian(node);
138+
139+
double fwd[6] = {5.0, 5.0, 12.0, 21.0, 21.0, 32.0};
140+
mu_assert("forward fail", cmp_double_array(node->value, fwd, 6));
141+
142+
/* row k: b[idx[k]] at col idx[k], a[idx[k]] at col 4 + idx[k] */
143+
double vals[12] = {5.0, 1.0, 5.0, 1.0, 6.0, 2.0, 7.0, 3.0, 7.0, 3.0, 8.0, 4.0};
144+
int rows[7] = {0, 2, 4, 6, 8, 10, 12};
145+
int cols[12] = {0, 4, 0, 4, 1, 5, 2, 6, 2, 6, 3, 7};
146+
147+
mu_assert("vals fail", cmp_values(node->jacobian, vals, 12));
148+
mu_assert("sparsity fail", cmp_sparsity(node->jacobian, rows, cols, 6, 12));
149+
free_expr(node);
150+
return 0;
151+
}
152+
121153
const char *test_jacobian_elementwise_mult_4(void)
122154
{
123155
// var = (z, x, w, y) where z is 2 x 1, x is 3 x 1, w is 2 x 1, y is 3 x 1

tests/wsum_hess/bivariate_full_dom/test_multiply.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,40 @@ const char *test_wsum_hess_multiply_linear_ops(void)
188188
return 0;
189189
}
190190

191+
const char *test_wsum_hess_multiply_duplicate_gathers(void)
192+
{
193+
/* Hessian of w' (a[idx] * b[idx]) with DUPLICATED gather indices
194+
* idx = [0, 0, 1, 2, 2, 3] (cvxpy#3442). a at offset 0, b at offset 4.
195+
* d²/da[p] db[p] accumulates w_k over every k with idx[k] == p:
196+
* H[p, 4+p] = {1+2, 3, 4+5, 6} = {3, 3, 9, 6}, plus the symmetric block. */
197+
double u_vals[8] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};
198+
double w[6] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
199+
int indices[6] = {0, 0, 1, 2, 2, 3};
200+
201+
expr *a = new_variable(4, 1, 0, 8);
202+
expr *b = new_variable(4, 1, 4, 8);
203+
expr *ga = new_index(a, 1, 6, indices, 6);
204+
expr *gb = new_index(b, 1, 6, indices, 6);
205+
expr *node = new_elementwise_mult(ga, gb);
206+
207+
node->forward(node, u_vals);
208+
jacobian_init(node);
209+
node->eval_jacobian(node);
210+
wsum_hess_init(node);
211+
node->eval_wsum_hess(node, w);
212+
213+
int expected_p[9] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
214+
int expected_i[8] = {4, 5, 6, 7, 0, 1, 2, 3};
215+
double expected_x[8] = {3.0, 3.0, 9.0, 6.0, 3.0, 3.0, 9.0, 6.0};
216+
217+
mu_assert("sparsity fail",
218+
cmp_sparsity(node->wsum_hess, expected_p, expected_i, 8, 8));
219+
mu_assert("vals fail", cmp_values(node->wsum_hess, expected_x, 8));
220+
221+
free_expr(node);
222+
return 0;
223+
}
224+
191225
const char *test_wsum_hess_multiply_2(void)
192226
{
193227
// Total 12 variables: [?, ?, ?, y0, y1, y2, ?, ?, x0, x1, x2, ?]

0 commit comments

Comments
 (0)