Skip to content

Fix use-after-free in BigInt self-addition and self-subtraction#9122

Open
tautschnig wants to merge 2 commits into
diffblue:developfrom
tautschnig:bigint-self-add
Open

Fix use-after-free in BigInt self-addition and self-subtraction#9122
tautschnig wants to merge 2 commits into
diffblue:developfrom
tautschnig:bigint-self-add

Conversation

@tautschnig

Copy link
Copy Markdown
Collaborator

operator+= and operator-= pass the right-hand side's digit buffer into the private add() method, which resizes *this before reading from that buffer. When the right-hand side is *this itself (x += x, x -= x) and the result requires a larger buffer, resize replaces and frees the digit array, and add() then reads the operand's digits from freed memory. Address sanitizer flags the read; the computed value may be silently wrong.

Detect the self-aliased case, which is exactly dig == digit since distinct BigInt objects never share digit buffers, and re-derive the operand pointer after the resize, whose buffer replacement preserves the contents. The bug went unnoticed because no in-tree code self-adds a BigInt, and buffers constructed via the numeric constructors carry enough spare capacity to make small self-additions stay in place; scanning from a decimal string produces a buffer without slack, making the reallocation reachable.

Add unit tests for x += x and x -= x with such a tight buffer, plus pin-down tests for x *= x, x /= x and x %= x, which were already safe: multiplication computes into a fresh buffer, division and remainder take an early equal-operands exit.

  • Each commit message has a non-empty body, explaining why the change was made.
  • n/a Methods or procedures I have added are documented, following the guidelines provided in CODING_STANDARD.md.
  • n/a The feature or user visible behaviour I have added or modified has been documented in the User Guide in doc/cprover-manual/
  • Regression or unit tests are included, or existing tests cover the modified code (in this case I have detailed which ones those are in the commit message).
  • n/a My commit message includes data points confirming performance improvements (if claimed).
  • My PR is restricted to a single feature or bugfix.
  • n/a White-space or formatting changes outside the feature-related changed lines are in commits of their own.

@tautschnig tautschnig self-assigned this Jul 19, 2026
Copilot AI review requested due to automatic review settings July 19, 2026 23:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a use-after-free bug in CBMC’s BigInt implementation when performing self-aliased compound addition/subtraction (x += x, x -= x) by ensuring the operand digit pointer is re-derived after a potential reallocation in resize(). It also adds unit tests that reproduce the original failure mode (tight buffer from decimal-string scanning) and pins down behavior of other self-aliased operations that were already safe.

Changes:

  • Fix BigInt::add() to handle the self-aliased operand case safely across resize() reallocation.
  • Add unit tests covering x += x and x -= x with a string-scanned value that forces reallocation.
  • Add “pin-down” unit tests for x *= x, x /= x, and x %= x self-operations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/big-int/bigint.cc Re-derives aliased operand pointer after resize() to prevent UAF in self-add/self-sub.
unit/big-int/big-int.cpp Adds regression and pin-down unit tests for self-aliased compound assignments and related ops.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread unit/big-int/big-int.cpp
Comment thread unit/big-int/big-int.cpp
operator+= and operator-= pass the right-hand side's digit buffer into
the private add() method, which resizes *this before reading from that
buffer. When the right-hand side is *this itself (x += x, x -= x) and
the result requires a larger buffer, resize replaces and frees the
digit array, and add() then reads the operand's digits from freed
memory. Address sanitizer flags the read; the computed value may be
silently wrong.

Detect the self-aliased case, which is exactly dig == digit since
distinct BigInt objects never share digit buffers, and re-derive the
operand pointer after the resize, whose buffer replacement preserves
the contents. The bug went unnoticed because no in-tree code
self-adds a BigInt, and buffers constructed via the numeric
constructors carry enough spare capacity to make small self-additions
stay in place; scanning from a decimal string produces a buffer
without slack, making the reallocation reachable.

Add unit tests for x += x and x -= x with such a tight buffer, plus
pin-down tests for x *= x, x /= x and x %= x, which were already safe:
multiplication computes into a fresh buffer, division and remainder
take an early equal-operands exit. The compound assignments are
applied through a reference, as clang's -Wself-assign-overloaded
would reject some of the very patterns under test when spelled
directly.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Like add(), the private mul() method takes a raw digit buffer that
aliases this->digit whenever a caller squares a number via x *= x.
This is not hypothetical: pow() in bigint-func.cc squares its base via
a *= a on every iteration, and the existing unit tests reach mul()
through it thousands of times. That this works is down to
implementation details of mul(): the single-digit path reads dig[0] by
value before mutating, the small-this path is unreachable for equal
lengths, and the general path multiplies into a fresh buffer before
releasing the old one. Any restructuring of mul() could quietly
introduce the same use-after-free just fixed in add().

Instead of relying on inspection, guard the aliased case structurally:
operator*= squares via a copy of the operand, making it impossible for
mul() to read freed memory no matter how it is implemented. The cost
is one pointer comparison per multiplication and one buffer copy in
the self-multiplication case only. Document the differing aliasing
contracts on the add() and mul() declarations.

Extend the self-aliased unit tests with a squaring of a value whose
buffer has no spare capacity, mirroring the x += x regression test.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.83%. Comparing base (ff8e112) to head (de3cbbe).
⚠️ Report is 2 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #9122      +/-   ##
===========================================
+ Coverage    80.77%   80.83%   +0.05%     
===========================================
  Files         1712     1715       +3     
  Lines       189833   189981     +148     
  Branches        73       73              
===========================================
+ Hits        153347   153570     +223     
+ Misses       36486    36411      -75     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tautschnig tautschnig assigned kroening and unassigned tautschnig Jul 20, 2026
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.

3 participants