Repository navigation
Keep GroebnerInputIdealStream's exponents in its Pimpl, so nothing leaks - #94
Merged
d-torrance merged 1 commit intoSep 25, 2026
Merged
Conversation
The constructor allocated two things in its mem-initializer list: the exponent array behind mExponents, then the Pimpl. If the Pimpl's allocation or constructor threw, the stream was never constructed, its destructor never ran, and the array leaked. Nothing on that path throws on a configuration GroebnerConfiguration has accepted, so only allocation failure reaches this -- most plausibly a very large variable count, where the array fits and a later allocation does not. The Pimpl now owns the array, as a unique_ptr<Exponent[]>, and mExponents points into it rather than owning memory of its own. With a single allocation there is nothing to clean up by hand: if it throws, nothing else exists yet. The public constructor makes the Pimpl first and delegates to a new private constructor, which sets both members and cannot fail; that ordering is needed because mExponents is declared, and so initialized, before mPimpl. The destructor's delete[] goes. The array is allocated with new Exponent[n], as before, rather than held in a std::vector: for zero variables a vector's data() may be null, and MonoMonoid::setExternalExponents asserts a non-null pointer, which OneIdealGB hits in Debug. The destructor's and debugAssertValid()'s mExponents != 0 checks become the invariant that now holds, mExponents == mPimpl->exponents.get(). mathicgb.h keeps mExponents a raw pointer at the same offset, so the inline appendExponent, compiled into callers, is unaffected and the layout is unchanged. The header gains only the private constructor's declaration. No soname change is needed: existing binaries never call the new constructor, and neither does new code outside the library. Checked by replacing operator new in a scratch program with one that fails on the n-th allocation, and constructing a stream failing each of the constructor's allocations in turn. On master, failures 2 through 10 each leak one allocation, the array. With this change none leak, in Release (10 allocations) or Debug (11). No test is added, since doing this in the unit-test binary would replace operator new for all of it, disabling ASan's own new/delete checking there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d-torrance
force-pushed
the
input-stream-exponents-leak
branch
from
September 25, 2026 21:07
4b54557 to
42ce3f8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GroebnerInputIdealStream's constructor allocated two things in itsmem-initializer list:
If the second throws, the stream is never constructed, its destructor never
runs, and the exponent array -- which only that destructor frees -- leaks.
Nothing on that path throws on a configuration
GroebnerConfigurationhasaccepted, so only allocation failure reaches it. The likeliest real case is a
very large variable count, where the array fits and a later allocation does
not.
The fix: one allocation, one owner
The
Pimplnow owns the exponent array, as aunique_ptr<Exponent[]>, andmExponentspoints into it instead of owning memory of its own. With a singleallocation there is nothing to clean up by hand: if it throws, nothing else
exists yet.
mExponentsis declared, and so initialized, beforemPimpl, but it now needsthe
Pimplto exist first. So the public constructor makes thePimplanddelegates to a new private constructor, which sets both members and cannot
fail:
The destructor's
delete[] mExponentsgoes.A
try/catchthat freed the array and rethrew would also have worked, with noheader change at all, but it hardcodes the cleanup at the one place it is needed
and stays correct only as long as nobody adds another allocation after it.
Why not
std::vectororstd::unique_ptrformExponentsitselfappendExponentis inline, so callers compile itsmExponents[index] = exponentinto their own code, and
mathicgb.hkeeps that member a raw pointer so thecaller's idea of the layout never depends on its standard library. That is
unchanged:
mExponentsis still a raw pointer at the same offset.Inside the
Pimplthe array isnew Exponent[n], as before, rather than astd::vector: for zero variables a vector'sdata()may be null, andMonoMonoid::setExternalExponentsasserts a non-null pointer, whichOneIdealGBhits in Debug. The twomExponents != 0asserts become theinvariant that now holds,
mExponents == mPimpl->exponents.get().ABI
No layout change and no soname change. Existing binaries never call the new
private constructor, and neither does new code outside the library -- only the
library's own public constructor does. It is one added exported symbol.
Verification
254/254 in cmake Release and Debug, GCC 13.3, no build warnings.
The full suite under ASan passes, and its leak report is byte-for-byte the
same as master's: 1216 bytes in 8 allocations, all from a test helper this PR
does not touch.
Fault injection, in a scratch program that replaces
operator newwith onethat fails on the n-th allocation, constructing a stream failing each of the
constructor's allocations in turn:
Pimpland what its constructor allocatesThat also shows nothing inside the
Pimpl's constructor leaks.No test is added. Doing the above in the unit-test binary would replace
operator newfor the whole suite, which also turns off ASan's own new/deletechecking there.
No autotools
distcheckrun locally; the commit touches no build system and CIcovers it.
🤖 Generated with Claude Code