Skip to content

Fix uninitialised read in lut_map on networks with dangling nodes - #707

Open
marcelwa wants to merge 50 commits into
lsils:masterfrom
marcelwa:fix-lutmap-uninit-cut
Open

Fix uninitialised read in lut_map on networks with dangling nodes#707
marcelwa wants to merge 50 commits into
lsils:masterfrom
marcelwa:fix-lutmap-uninit-cut

Conversation

@marcelwa

Copy link
Copy Markdown
Contributor

lut_map reads uninitialised memory, and usually segfaults, on any network that carries nodes unreachable from the outputs. lut_map_impl::compute_share_mapping_init loops over every node index and calls cuts[i].best(), but the cut enumerator never visits an unreachable node, so its cut set is still empty and best() returns *_pcuts[0], which points at a default-constructed cut; cut() = default leaves _length, _cend and _end indeterminate, so the for ( auto leaf : cut ) in compute_cut_data walks a garbage end pointer and then indexes cuts[leaf]. This is easy to hit from outside without doing anything unusual: every AIG that ABC writes after &dch -f; &put keeps the choice-class members as ordinary AND nodes (cavlc comes out with 1271 ANDs of which 647 are reachable), so anyone piping ABC into mockturtle trips it, and because it is undefined behaviour rather than a check it can also just produce plausible-looking numbers instead of crashing. The fix here gives cut's default constructor a defined empty state rather than guarding the caller, on the reasoning that a default-constructed cut sitting in cut_set::_cuts and reachable through best() ought to be readable at all; the regression test added to test/algorithms/lut_mapper.cpp maps a six-gate AIG whose last four gates drive no output, and it exits with signal 11 without the change and passes with it. If you would rather have the guard in lut_mapper.hpp — skipping empty cut sets in compute_share_mapping_init, which is what we carried locally first — or want the initialisation written as default member initialisers instead, say so and we will redo it.

marcelwa and others added 30 commits June 27, 2024 19:20
…ly caused issues with _x86, which is used by Windows.
…ly caused issues with _x86, which is used by Windows.
🚨 Fix compilation error for apple clang 15.0.7
🐛 Added missing namespace ``std::chrono``.
👷 Modernized the Linux CI system for more extensive testing across OS and compiler versions

👷 Adjust PR triggers

👷 Adjust PR triggers

⏪ Revert triggers

⬆️ Upgrade phmap dependency to the latest version

⏪ Removed sanitizers

✅ Fixed tests
* 👷 Cancel in-progress workflows on newer refs or canceled builds

* 👷 Modernize macOS CI workflow

* 👷 Modernize Windows CI workflow

* 🎨 Formatting

* 💚 Try to fix macOS runs

* 💚 Attempt to fix CI build

* 👷 Re-enable concurrency restrictions for macOS CI

* 🎨 Update CMakeLists.txt to add UTF-8 support for MSVC compile options

* 💚 Update the Windows CI to use PowerShell for shell execution

* 💚 Attempt to fix test execution command in Windows CI
wjrforcyber and others added 19 commits April 30, 2025 16:56
* 🐛 Fix(aag): aag check with invalid index

Fixes marcelwa/aigverse#131

Signed-off-by: JingrenWang <wjrforcyber@163.com>

* ♻️ Refactor(Var name): Varible name change

Signed-off-by: JingrenWang <wjrforcyber@163.com>

* ♻️ Refactor(Non-negative Check): Check value of `long` is non-negative before bound check

Signed-off-by: JingrenWang <wjrforcyber@163.com>

* ♻️ Refactor(Comment): Refactor description.

Signed-off-by: JingrenWang <wjrforcyber@163.com>

* ♻️ Refactor(Typo): Fix typo according to review

Signed-off-by: JingrenWang <wjrforcyber@163.com>

* ♻️ Refactor(TypeCast): `uint64_t` to `long`

Signed-off-by: JingrenWang <wjrforcyber@163.com>

---------

Signed-off-by: JingrenWang <wjrforcyber@163.com>
Co-authored-by: Marcel Walter <marcel.walter@tum.de>
Signed-off-by: JingrenWang <wjrforcyber@163.com>
# Conflicts:
#	.github/workflows/linux.yml
#	.github/workflows/macos.yml
#	.github/workflows/windows.yml
The build workflows still passed the pre-rename CMake option names
(`MOCKTURTLE_TEST`, `MOCKTURTLE_EXAMPLES`, `MOCKTURTLE_EXPERIMENTS`), which have
been no-ops since the options became `MOCKTURTLE_BUILD_*`. `MOCKTURTLE_BUILD_TESTS`
defaults to OFF, so `run_tests` was never built and every job failed at the
"Run tests" step with "working directory build/test: No such file or directory".
The test suite has effectively not run in CI since the rename; `coverage.yml`
was migrated at the time, the three build workflows were missed.

Windows additionally targeted images that no longer exist:

- `windows-2019` was retired as a GitHub-hosted image; its jobs never started.
- `windows-2025` ships Visual Studio 2026 since June 2026, so the
  "Visual Studio 17 2022" generator fails with "could not find any instance of
  Visual Studio". Visual Studio 2022 is now only on `windows-2022`.

The v142 toolset went away with windows-2019, so the matrix is now spelled out
explicitly instead of being built from a cross product with exclusions.

Also set `fail-fast: false` on all three matrices. A single failing job was
cancelling the other 24 and reporting them as failures, which hid what was
actually broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
`clone a block network` asserted `ntk3.size() == 7` and `ntk3.num_gates() == 3`
for a deep copy of a network that has size 6 and 2 gates. Those numbers encoded
the double-initialization bug that "Fixed double initialization of cloned block
networks" removed: `clone()` re-ran `_init()` on the copied storage and appended
a second constant node.

With that fix in place a clone is now an exact snapshot of the source network,
so the expectations become 6 and 2. `num_pos()` was already correct.

This went unnoticed because the test suite has not actually been built in CI --
see the preceding commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
`write_aiger` hardcoded a latch count of 0 and asserted that the network was
combinational, so `sequential<aig_network>` could be passed in but silently
lost its registers: they degraded into extra PI/PO pairs.

Emit registers as AIGER latches when the network type provides `num_registers`,
`foreach_ri`, and `foreach_ro`. The combinational path is unchanged and still
produces byte-identical output.

Reset values are always written explicitly. An omitted reset field means 0 in
the AIGER format, so omitting it would silently turn an uninitialized register
into a zero-initialized one; an undefined reset is instead encoded by repeating
the latch's own current-state literal, as the format prescribes. Latch names are
emitted as `l<n>` symbol table entries, mirroring `on_latch_name`.

Because the binary format encodes CIs implicitly, PIs must be variables 1..I and
register outputs I+1..I+L. That invariant is now checked by a debug assertion
rather than being silently assumed.

Also add the missing `<cassert>` include; the header used `assert` but only
compiled because its includers happened to provide it.

Verified against both readers: the output round-trips through `aiger_reader`
preserving register count, reset values, and names, and ABC reads it back with
the expected `Init0`/`Init1`/`InitDC` counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
A sequential network written by ABC could not be read back correctly. Three
separate defects, all in the AIGER latch and output handling.

**Omitted latch reset values were read as undefined.** Both the binary and the
ASCII reader defaulted to NONDETERMINISTIC when a latch line carried no reset
field. The format specifies the opposite: the original AIGER format initialized
every latch to zero, and the reset field added in 1.9 is optional, so its absence
means 0. ABC relies on this and omits the field for zero-initialized latches, so
every zero-initialized register came back undefined.

**The ASCII reader compared the wrong token.** The branch recognizing a reset
value of 1 tested `tokens[1u]`, the next-state literal, instead of `tokens[2u]`.
An explicit reset of 1 was therefore never recognized, and a latch whose
next-state literal happened to be 1 was wrongly reported as one-initialized.

**Bad state properties were dropped.** A writer emitting the AIGER 1.9 extended
header moves the primary outputs into the bad-state section; ABC does this for
any design with a non-zero latch initialization. `on_bad_state` was not
overridden, so those outputs disappeared and the network came back with none.
They are now kept as primary outputs, which is what they were, along with their
names from the symbol table.

Also fix an adjacent defect in the destructor: the output index only advanced for
outputs that carried a name, so with a sparse symbol table every name after the
first unnamed output landed on the wrong one.

Verified end to end against ABC. A network with three registers initialized to
0, 1, and undefined previously returned with no outputs and a corrupted first
register; it now round-trips through `resyn2` with its inputs, outputs,
registers, and reset values intact.

Note that AIGER constraints are still ignored. They are assumptions rather than
outputs, so mapping them onto primary outputs would change the meaning of the
network.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
`register_t::init` carried three different encodings of "undefined" at once.
`register_t` itself defaulted to 3, `blif_reader` used 2 for a nondeterministic
latch and 3 for an unspecified one, and `aiger_reader` produced 255 -- the
result of narrowing an `int8_t` of -1 into a `uint8_t` field rather than a
deliberate choice.

That last one corrupted output. `write_blif` emits the initialization verbatim
and the BLIF `.latch` statement accepts only 0, 1, 2, and 3, so a sequential
AIGER file with an undefined latch reset read back and written as BLIF produced

    .latch li0 new_n2   255

which no BLIF consumer accepts, ABC included.

Introduce `register_init` with the four documented values, following the BLIF
`.latch` field since it is the most expressive of the supported formats, and use
it consistently across the readers and writers. AIGER has no counterpart for
`unknown`, so a latch without a defined reset maps to `dont_care`.

`register_init::is_defined` expresses the test that callers actually want,
namely whether a reset value is 0 or 1, so code stays correct if a format ever
introduces further undefined states. `register_init::sanitize` keeps `write_blif`
from emitting a value the format cannot represent.

The only value that changes is the one AIGER produced for a nondeterministic
latch, from 255 to 2. Comparisons of the form `init > 1` are unaffected; only
code testing against 255 would notice, and that value was never intentional.

Verified end to end: the AIGER file above now yields `.latch li0 new_n2   2`,
which ABC reads back as one don't-care-initialized latch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
# Conflicts:
#	.github/workflows/macos.yml
#	include/mockturtle/io/write_aiger.hpp
`aiger_reader::on_header` only materialized the latch outputs when the network
type implements `create_ro`. For one that does not, the reader's `signals`
vector was left short by exactly the latch count, while every AIGER literal
above the primary inputs still assumed those slots existed. `on_and` then
indexed past the end of the vector and handed the garbage it read to
`create_and`.

The only thing standing between that and undefined behaviour was

    assert( num_latches == 0 && "network type does not support the creation of latches" );

which is compiled out under `NDEBUG` -- that is, in every release build. The
result was a segmentation fault on any design whose logic reaches far enough
past the latch outputs, and a silently wrong network on any that does not: a
one-latch file whose literals all stay in bounds parsed "successfully" into a
network missing its registers entirely.

Reading a latched file into a combinational network now flattens one timeframe
of the design instead. Each latch output becomes a primary input, appended
behind the file's own primary inputs, and each latch next-state function becomes
a primary output, appended behind the file's own primary outputs. That is the
transformation ABC calls `comb`; it loses no logic, keeps every literal
resolving to the signal the file names, and makes the network type in this
reader's own documented example -- `mig_network`, which has no registers -- work
on a sequential file rather than crash on one.

Recording and naming latches is no longer conditional on the network type
either, since the flattened path needs both. The header and the destructor also
disagreed about which trait decides whether a network can hold registers,
`create_ro` in one and `create_ri` in the other; both now consult a single
`has_registers` predicate, which is what let them drift apart to begin with.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* ✨ Add cycle-accurate simulation of sequential networks

`simulate` evaluates the combinational logic of a network exactly once and has
no notion of a register. Handed a `sequential<Ntk>` it assigns the constants,
the primary inputs and the gates -- but never the register outputs, which are
neither. They keep whatever `node_map` default-constructed them to, so every
value in their fanout cone is meaningless: constant 0 for a `bool` simulation,
a zero-variable truth table otherwise. Nothing warns about it.

`simulate_sequential` runs the network over a number of clock cycles instead.
Every register starts at its reset value, the combinational logic is evaluated
once per cycle, the primary outputs are recorded, and the register inputs are
latched into the register outputs for the next cycle.

The simulator concept is unchanged, with one addition: a simulator that provides
`compute_pi( index, cycle )` is driven per cycle, so the primary inputs can
change from one to the next. `stimulus_simulator` is such a simulator, holding
one assignment vector per cycle and repeating its last one for the rest of the
run. Everything that works with `simulate` keeps working, holding its assignment
for the whole run -- which is what a design with no primary inputs, an LFSR say,
wants anyway.

A register may declare no reset value at all -- `register_init::dont_care` or
`register_init::unknown`, which is what `register_t` defaults to and what an
AIGER latch with a nondeterministic reset reads back as. Simulation needs a
concrete one, so `simulate_sequential_params::undefined_reset_value` says which.

It lives in its own header rather than in `simulation.hpp` because it needs
`networks/sequential.hpp`, which pulls in every network implementation. Putting
it in `simulation.hpp` would put that cost on every translation unit that
simulates anything.

Seven test cases: a 4-bit LFSR walking all 15 of its non-zero states and
returning to its seed on the sixteenth, a second seed producing the same
sequence one step ahead (which only holds if the reset values are honoured), a
shift register driven by a per-cycle stimulus, a stimulus shorter than the run,
an undefined reset following the parameter both ways, truth-table simulation,
and a zero-cycle run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 💚 Fix the stimulus construction GCC 12 and older reject

`stimulus_simulator sim( { { true } } )` is ambiguous on GCC 12 and older: a
single-element braced list is as good a match for the copy constructor as for
the one taking the stimulus. Every g++ job up to 12 failed on it while every
clang job and g++ 13 and 14 passed.

Construct it from a named vector, which leaves only one viable candidate on
every compiler. The two-element spelling a few tests up is unambiguous and
stays as it is, since it is the form a reader should copy.

* ♻️ Return a result struct from simulate_sequential

`std::vector<std::vector<SimulationType>>` says nothing about how the data is
laid out -- which index is the cycle and which the output is something a caller
had to learn from the documentation and then remember. Return a
`simulate_sequential_result` that names both axes instead.

It carries the register values as well, which the loop had all along and threw
away. `outputs[cycle][index]` is what primary output `index` emitted in that
cycle, `states[cycle][index]` what register `index` held while that cycle was
evaluated.

The state trace is one entry longer than the output trace, because simulating
`n` cycles crosses `n + 1` state boundaries. `reset_state()` is the one the run
started from and `final_state()` the one it ended in, which also gives a run of
zero cycles something sensible to report: no outputs, and the reset state.

Three new test cases: the fencepost between the two traces, the LFSR walking
fifteen distinct non-zero states and returning to its seed, and a register read
out one cycle after it was written, where the output of each cycle must equal
the state it started in. The truth-table case now checks the register as well as
the output.

Also spell the simulator in the header's example as
`default_simulator<bool>( std::vector<bool>{} )`. `( {} )` is ambiguous on GCC 12
and older, and an example is the last place to put something a reader cannot
compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The six format headers declare their compiled patterns `static` at namespace
scope. `static` gives internal linkage, so every translation unit that includes
aiger.hpp gets its own private set of nine std::regex objects, each constructed
during that TU's static initialization. Linking two such TUs shows 18 distinct
objects where the source describes 9; the C++17 spelling for one shared
definition is `inline`.

`const` matters more than the duplication does. These live in public namespaces
under public names, and nothing but convention stops a caller reassigning one.
Every use is std::regex_match or an sregex_iterator constructor, both of which
take a const reference, so the objects are already read-only in practice --
which is what makes concurrent parsing safe. Spelling them const promotes that
from a property of today's call sites, which has to be re-established by reading
every one of them, to a guarantee the compiler enforces. aigverse released the
GIL around these parsers on exactly that reasoning.

29 patterns across aiger, pla, verilog, bench, blif and dimacs. No call site
changes: this is a declaration-only patch.
`cut() = default` leaves `_length`, `_cend` and `_end` indeterminate.  A
default-constructed cut is reachable: `cut_set` and `lut_cut_set` hold an array
of them and `best()` returns `*_pcuts[0]` whether or not any cut has been
inserted.  `lut_map_impl::compute_share_mapping_init` iterates over every node
index and calls `best()`, and a node unreachable from the outputs is never
visited by the cut enumerator, so its cut set is empty and the following
`for ( auto leaf : cut )` in `compute_cut_data` walks a garbage end pointer and
indexes `cuts[leaf]` with whatever it finds.

Networks with unreachable nodes are not exotic: ABC's `&dch -f; &put` leaves the
choice-class members in as ordinary AND nodes, so every AIG written by the
standard `strash; &get; &dch -f; &put; write_aiger` front end has them (cavlc:
1271 ANDs written, 647 reachable).

Give the default constructor a defined empty state, and add a lut_mapper test
that maps a six-gate AIG whose last four gates drive no output.
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.03%. Comparing base (0886ebf) to head (8d96a76).

Files with missing lines Patch % Lines
...de/mockturtle/algorithms/simulation_sequential.hpp 96.42% 2 Missing ⚠️
include/mockturtle/utils/progress_bar.hpp 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #707      +/-   ##
==========================================
- Coverage   84.06%   84.03%   -0.04%     
==========================================
  Files         190      191       +1     
  Lines       29515    29588      +73     
==========================================
+ Hits        24812    24864      +52     
- Misses       4703     4724      +21     

☔ 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.

# Conflicts:
#	include/mockturtle/io/aiger_reader.hpp
#	test/io/aiger_reader.cpp
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