Skip to content

boxstacks: fix out-of-range fixed_items_solutions index in sequential_onedimensional_rectangle - #541

Open
HansBug wants to merge 2 commits into
fontanf:masterfrom
HansBug:fix/boxstacks-axle-index
Open

boxstacks: fix out-of-range fixed_items_solutions index in sequential_onedimensional_rectangle#541
HansBug wants to merge 2 commits into
fontanf:masterfrom
HansBug:fix/boxstacks-axle-index

Conversation

@HansBug

@HansBug HansBug commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #537

Summary

  • When a boxstacks bin is a semi-trailer truck and the first packing violates the middle axle weight limit, the axle weight repair loop in sequential_onedimensional_rectangle left fixed_items_solutions_pos equal to fixed_items_solutions.size(), and the next iteration read one element past the end of the vector. Copy-constructing a Solution from that memory threw std::bad_array_new_length, or std::bad_alloc depending on the objective.
  • The scan that recomputes the index skipped its break on the last entry, because the pos + 1 < fixed_items_solutions.size() guard is false there, so the counter was incremented one time too many.

Changes

src/boxstacks/sequential_onedimensional_rectangle.cpp — one condition, so the scan breaks on the last entry instead of running past it:

                if (pos + 1 >= (ItemPos)fixed_items_solutions.size()
                        || fixed_items_solutions[pos + 1].x_max() > xi - x_max)
                    break;

Nothing else was needed. The clamp below it only raises the index to the lower bound, and the loop's own end condition covers the case where the last entry does fail the length test: the lower bound then becomes size() and the loop breaks before anything is indexed.

Test plan

  • Full build, default configuration (-DCMAKE_BUILD_TYPE=Release, CLP and HiGHS both on, liblapack-dev and libbz2-dev installed, as in .github/workflows/build.yml).
  • ctest --parallel 8 in build/test: 596 tests pass, unchanged from master.
  • The reproducer from [boxstacks] Out-of-range read in the axle-weight repair loop of sequential_onedimensional_rectangle (std::bad_array_new_length) #537, built from this branch: 3/3 runs exit 0 and write a certificate. The same build with this commit reverted: 3/3 runs fail with Error: std::bad_array_new_length.
  • Same reproducer with --objective knapsack and --objective feasibility, which fail with std::bad_array_new_length and std::bad_alloc on master: both exit 0 with this change.
  • Confirmed the index really was out of range before the change, by temporarily adding a bounds check in front of the indexing: it reports fixed_items_solutions_pos 2 with fixed_items_solutions.size() 2 on the second iteration.

I did not add a regression test. Reaching this code needs a full optimize run on a semi-trailer instance, and neither test/boxstacks nor data/boxstacks currently has a data-driven optimize suite to hang it on. If you would like one, I am glad to add it in the shape you prefer, either a data/boxstacks/tests/... set like rectangleguillotine has, or a unit test on the loop extracted into its own function.

As noted in #537, this stops the crash but the instance still returns an empty solution, which looks like a separate limitation of the repair strategy. This PR does not touch that.

Environment: Ubuntu 24.04.3 LTS, x86-64, glibc 2.39, GCC 13.3.0, CMake 4.4.3.

…_onedimensional_rectangle

When the middle axle weight constraint fails, the repair loop rescans
fixed_items_solutions for the position to fix next. On the last entry,
'pos + 1 < fixed_items_solutions.size()' is false, so the break was
skipped and the counter was incremented once more, leaving
fixed_items_solutions_pos equal to fixed_items_solutions.size() whenever
no earlier break happened. The clamp that follows only raises the index
to the lower bound and the loop's end condition only breaks when the
lower bound passes the upper bound, so the next iteration indexed one
past the end and copy-constructed a Solution from that memory, which
threw std::bad_array_new_length (std::bad_alloc for some objectives).

Break on the last entry instead of running past it, so the index stays
within the vector.
@fontanf

fontanf commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Hi,

Thank you for the contribution.

Do you have a failing test case for this?

Two instances that enter the repair loop of
sequential_onedimensional_rectangle, in a new parametrized suite over
instance files, following the other parametrized tests of the project.
Both throw std::bad_array_new_length without the fix.

The bin packing case covers the objective the crash was reported with;
the repair returns an empty solution for it, so the case only pins down
that optimize returns at all. The knapsack case is the one that also
pins down a non-empty result. The expected number of items is a
minimum rather than an exact count, so the test does not have to be
updated if the repair later packs more items.

boxstacks has no SolutionBuilder::read, so the solutions are checked
this way rather than against reference certificates.
@HansBug

HansBug commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Yes, added as a second commit, in the same shape as the tests on #540.

data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing and data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack hold three pallets in a 13.6 m semi-trailer with a middle axle limit the leftmost packing exceeds, which is what sends sequential_onedimensional_rectangle into the axle weight repair loop. Both throw std::bad_array_new_length on master and pass with the fix, in test/boxstacks/axle_weight_test.cpp.

Two cases rather than one, because they pin down different things. The bin packing case is the objective the crash was reported with, but the repair returns an empty solution for it even after the fix, so on its own it would only prove that optimize returns. The knapsack case returns a real packing, so it also catches a regression that made the crash go away without the search still working.

The expected number of items is a minimum rather than an exact count, so the test does not need updating if the repair later packs more items than it does today.

Same caveat as on #540: boxstacks has no SolutionBuilder::read, so the solutions are checked this way rather than against reference certificates. I put the suite in its own file so that the two pull requests stay independent of each other; if you prefer a single boxstacks optimize suite, they are trivial to fold together once one of them is in.

Full build in the default configuration, ctest --output-on-failure --parallel 8: 598 tests pass, which is the 596 on master plus these two.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[boxstacks] Out-of-range read in the axle-weight repair loop of sequential_onedimensional_rectangle (std::bad_array_new_length)

2 participants