Skip to content

Keep GroebnerInputIdealStream's exponents in its Pimpl, so nothing leaks - #94

Merged
d-torrance merged 1 commit into
Macaulay2:masterfrom
d-torrance:input-stream-exponents-leak
Sep 25, 2026
Merged

d-torrance merged 1 commit into
Macaulay2:masterfrom
d-torrance:input-stream-exponents-leak

Conversation

@d-torrance

Copy link
Copy Markdown
Member

GroebnerInputIdealStream's constructor allocated two things in its
mem-initializer list:

  mExponents(new Exponent[conf.varCount()]),
  mPimpl(new Pimpl(conf))

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 GroebnerConfiguration has
accepted, 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 Pimpl now owns the exponent array, as a unique_ptr<Exponent[]>, and
mExponents points into it instead of owning memory of its own. With a single
allocation there is nothing to clean up by hand: if it throws, nothing else
exists yet.

mExponents is declared, and so initialized, before mPimpl, but it now needs
the Pimpl to exist first. So the public constructor makes the Pimpl and
delegates to a new private constructor, which sets both members and cannot
fail:

GroebnerInputIdealStream::GroebnerInputIdealStream(
  const GroebnerConfiguration& conf
):
  GroebnerInputIdealStream(new Pimpl(conf))
{}

GroebnerInputIdealStream::GroebnerInputIdealStream(Pimpl* const pimpl):
  mExponents(pimpl->exponents.get()),
  mPimpl(pimpl)
{ ... }

The destructor's delete[] mExponents goes.

A try/catch that freed the array and rethrew would also have worked, with no
header 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::vector or std::unique_ptr for mExponents itself

appendExponent is inline, so callers compile its mExponents[index] = exponent
into their own code, and mathicgb.h keeps that member a raw pointer so the
caller's idea of the layout never depends on its standard library. That is
unchanged: mExponents is still a raw pointer at the same offset.

Inside the Pimpl the array is new Exponent[n], as before, rather than 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 two mExponents != 0 asserts become the
invariant 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 new with one
    that fails on the n-th allocation, constructing a stream failing each of the
    constructor's allocations in turn:

    master this branch
    failure 1, the array itself clean clean
    failures 2-10, the Pimpl and what its constructor allocates leaks the array, each time clean
    Debug, 11 allocations -- clean

    That also shows nothing inside the Pimpl's constructor leaks.

No test is added. Doing the above in the unit-test binary would replace
operator new for the whole suite, which also turns off ASan's own new/delete
checking there.

No autotools distcheck run locally; the commit touches no build system and CI
covers it.

🤖 Generated with Claude Code

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
d-torrance force-pushed the input-stream-exponents-leak branch from 4b54557 to 42ce3f8 Compare September 25, 2026 21:07
@d-torrance
d-torrance merged commit 33fd969 into Macaulay2:master Sep 25, 2026
18 checks passed
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.

1 participant