Skip to content

xilinx: fix use-after-free when a LUT6_2 feeds a MUXF7/8/9 tree - #148

Merged
hansfbaier merged 3 commits into
openXC7:stable-backportsfrom
jasonzeng124:fix/lut6-2-muxf-uaf
Aug 18, 2026
Merged

xilinx: fix use-after-free when a LUT6_2 feeds a MUXF7/8/9 tree#148
hansfbaier merged 3 commits into
openXC7:stable-backportsfrom
jasonzeng124:fix/lut6-2-muxf-uaf

Conversation

@jasonzeng124

Copy link
Copy Markdown

Summary

  • Fixes a use-after-free segfault when a LUT6_2 feeds a MUXF7/MUXF8/MUXF9 input. pack_muxfs()'s constrain_muxf_tree() walks driver.cell across nets with no type check and can set constr_parent/constr_children on a LUT6_2 before split_lut6_2() (previously run inside pack_luts(), after pack_muxfs()) deletes that exact cell — leaving other cells holding a dangling CellInfo* that the HEAP placer later dereferences.
  • Fix moves split_lut6_2() to run before pack_muxfs()/pack_carries()/pack_srls() in both the xc7 and Ultrascale flows, so LUT6_2 no longer exists as a type by the time any untyped driver-walk pass runs — this closes the bug class at its root rather than adding a type check to one call site.
  • flush_cells() now asserts a cell being deleted has no live constr_parent/constr_children, so any future instance of this class fails loudly at the deletion site instead of segfaulting later, deep in the placer.
  • constrain_lut6_2_pairs() no longer overwrites constr_parent on a half already constrained by an earlier pass (e.g. its own mux-tree constraint) — found via adversarial testing on top of the reorder: a LUT6_2 that is both constant-I5 pairable and drives a MUXF7 input hit the same dangling/desynced-constraint problem through a different path. Such a pair is now left unpaired (costs one extra LUT bel — the same tradeoff already accepted for the non-constant-I5 case) instead of corrupting constraint state.
  • split_lut6_2() now propagates the original cell's region constraint to both halves (previously silently dropped).
  • split_lut6_2() fails loudly (log_error) instead of silently misbehaving in two cases: a split-half name colliding with an existing cell (previously a bare NPNR_ASSERT deep inside flush_cells()), and a BEL-constrained LUT6_2 that drives both O5 and O6 (genuinely ambiguous — no rule for which half should keep the BEL). A BEL-constrained LUT6_2 using only one output now correctly inherits the BEL onto that single half, matching pre-split behaviour (this was a regression caught by an independent review pass — see below).

Scope

xilinx/pack.cc + xilinx/pack.h only. Two commits:

  1. 6d4e5df1 — the use-after-free fix (reorder, guards, region propagation, flush_cells assert).
  2. 0d9fda6a — follow-up fix for a regression an independent Opus review caught in commit 1: the BEL-constraint guard was over-broad and rejected a previously-working case (BEL-pinned LUT6_2 using only O6).

Testing

Verified against hand-built repro netlists (chipdb: xc7z010clg400), each hitting a distinct code path:

  • Original UAF: LUT6_2.O6 -> MUXF7. Segfaults before this fix; places and routes cleanly after.
  • Constant-I5 LUT6_2 with no mux involvement: confirms the pair still re-fuses onto one physical LUT (not two).
  • Paired LUT6_2 (constant I5) with O6 also feeding a MUXF7: exercises the reordered flow's interaction with mux constraining.
  • Same, but with O5 (not O6) feeding the MUXF7: the actual conflicting case for constrain_lut6_2_pairs()'s new guard — confirmed load-bearing by temporarily removing the guard and observing a different placer assertion fire (common/placer_heap.cc:1068).
  • Split-half name collision (<name>$LUT5 pre-existing): confirms the new log_error fires with a clear message instead of the old bare assert.
  • BEL-constrained LUT6_2 driving only O6: previously hard-errored (regression introduced by commit 1's original, over-broad BEL guard); now packs/places/routes cleanly.
  • BEL-constrained LUT6_2 driving both O5 and O6: confirms this still correctly errors (genuinely ambiguous case).

All cases place and route cleanly except the two intentional log_error cases, which fail with the expected, specific message.

Out of scope (documented, not fixed here)

  • A non-constant-I5 LUT6_2 still costs 2 LUT bels instead of 1 — the placer's fracturable-LUT legality check (arch_place.cc) refuses a 5LUT companion whenever the 6LUT genuinely uses all 6 inputs. Closing this needs an arch-level change, not a packer-level one, and doesn't affect correctness — a pre-existing, accepted limitation.
  • A constant-I5-pairable LUT6_2 whose O6 half single-fanout-drives a CARRY4 input can have that half adopted directly into the carry chain by pack_carries() before constrain_lut6_2_pairs() gets a chance to re-fuse the pair, permanently costing 2 LUT bels instead of 1. Not a correctness bug (no crash, no wrong output), just a LUT-utilization nit in a narrow corner case — deliberately left out of this PR.
  • A pre-existing, unrelated bug in constrain_muxf_tree(): its own reentrancy guard (curr->type == id_SLICE_LUTX && ...) can never fire for a plain LUT, because SLICE_LUTX cells don't exist yet at pack_muxfs() time. A LUT with fanout into two different mux trees can therefore still get double-constrained. This is unchanged by, and unrelated to, LUT6_2 — flagged by review for future work, not part of this PR.

Review

An independent Opus-model review pass was run against commit 1 specifically. It found the BEL-guard regression above (fixed in commit 2) and confirmed the reordering itself is sound: no pass between split_lut6_2() and pack_luts() deletes a LUT5/LUT6 half, and constrain_lut6_2_pairs()'s guard is symmetric and can't leave a half half-fixed.

jasonzeng124 and others added 2 commits August 14, 2026 13:37
pack_muxfs() runs before pack_luts() in both the xc7 and Ultrascale
flows. Its constrain_muxf_tree() walks driver.cell across nets with
no cell-type check, so a LUT6_2 feeding a MUXF7/8/9 input gets
constr_parent/constr_children set on it by the mux-tree constraint
walk. split_lut6_2() (previously called from inside pack_luts(),
which runs afterwards) then deletes that exact cell via flush_cells()
without repairing those pointers, leaving a dangling CellInfo* that
the HEAP placer later dereferences -- segfault.

Fix by running split_lut6_2() before pack_muxfs()/pack_carries()/
pack_srls() in both flows, so LUT6_2 no longer exists as a type by
the time any pass that net-walks into driver cells without a type
check gets to run. This closes the whole class rather than adding a
type check to constrain_muxf_tree() alone.

Also:
- flush_cells() now asserts a cell being deleted has no live
  constr_parent/constr_children links, so a future instance of this
  class of bug fails loudly at the deletion site instead of
  segfaulting later, deep in the placer.
- constrain_lut6_2_pairs() no longer overwrites constr_parent on a
  half that was already constrained by an earlier pass (e.g. its own
  mux-tree constraint) -- found via adversarial testing: the same
  LUT6_2 being both 5LUT/6LUT-pairable (constant I5) *and* driving a
  MUXF7 input hit the same class of dangling/desynced-constraint bug
  through a different code path (constrain_lut6_2_pairs unconditionally
  overwriting constr_parent), producing a different placer assertion
  failure. Such a pair is now left unpaired (costs an extra LUT bel,
  same tradeoff already accepted for the non-constant-I5 case) rather
  than corrupting constraint state.
- split_lut6_2() now propagates the original cell's region constraint
  to both halves (a region is just a bbox, so this is safe and cheap
  insurance against silently dropping it).
- split_lut6_2() fails loudly (log_error) instead of silently
  discarding state in two previously-unguarded cases: a LUT6_2 with a
  pre-existing BEL constraint (no defined rule for which physical half
  should inherit a single-bel pin), and a split-half name that
  collides with an existing cell (previously a bare NPNR_ASSERT deep
  in flush_cells()).

Verified against 4 hand-built repro netlists: the original UAF
(LUT6_2.O6 -> MUXF7), a paired constant-I5 LUT6_2 (still costs one
physical LUT), a paired LUT6_2 whose O6 also drives a MUXF7 input, and
the same with O5 driving the MUXF7 (the actual conflicting case for
constrain_lut6_2_pairs -- confirmed by reverting the guard and seeing
a different placer assertion fire). All four now place and route
cleanly.

Not fixed here (documented, left out of scope):
- Non-constant-I5 LUT6_2 still costs 2 LUT bels instead of 1; the
  placer's fracturable-LUT model refuses a 5LUT companion whenever the
  6LUT uses all 6 inputs (arch_place.cc), so closing this needs an
  arch-level change, not a packer-level one.
- A BEL constraint on a pre-split LUT6_2 has no defined per-half
  splitting rule; no live producer of this was found in this codebase,
  so it now fails loudly rather than being silently guessed at.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
split_lut6_2() previously log_error'd on any LUT6_2 carrying a BEL
attribute, regardless of whether O5 was actually used. That's a
regression versus pre-split behaviour: before split_lut6_2() existed,
a BEL-constrained LUT6_2 driving only O6 packed fine (it just aliased
straight onto the LUT6 xform rule, keeping its single port name and
BEL). Independent review caught this by diffing against the parent
commit.

A BEL names one physical resource; splitting is only genuinely
ambiguous when both O5 and O6 are driven, since then there's no rule
for which of the two resulting cells should inherit it. When only one
output is used, exactly one half is ever created, so it can just take
the BEL directly.

Verified: a BEL-constrained, O6-only LUT6_2 now packs/places/routes
cleanly (previously hard errored); a BEL-constrained LUT6_2 driving
both O5 and O6 still errors, as intended.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread xilinx/pack.cc Outdated
// inherits it below -- but if both O5 and O6 are driven there's no
// defined rule for which half should get it. Fail loudly rather than
// guess in that case.
if (ci->attrs.count(id_BEL) && o5 != nullptr && o6 != nullptr)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please extract the condition to a bool with a self documenting name

Comment thread xilinx/pack.cc Outdated
std::unique_ptr<CellInfo> half = create_cell(
ctx, ctx->id("LUT" + std::to_string(n_in)), ctx->id(ci->name.str(ctx) + suffix));
IdString half_name = ctx->id(ci->name.str(ctx) + suffix);
if (ctx->cells.count(half_name))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please extract the condition to a bool with a self documenting name

Comment thread xilinx/pack.cc Outdated
// The BEL-ambiguity check above guarantees at most one half is
// ever actually built when the original cell was BEL-constrained,
// so it's safe to hand that BEL straight to whichever one it is.
if (ci->attrs.count(id_BEL))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please extract the condition to a bool with a self documenting name

Comment thread xilinx/pack.cc Outdated
// pair unpaired rather than corrupt an existing constraint -- it
// costs the extra LUT bel split_lut6_2() already accepts for the
// non-constant-I5 case.
if (lut6->constr_parent != nullptr || lut6->constr_abs_z || !lut6->constr_children.empty() ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please extract the condition to two bools with a self documenting names

Addresses review feedback on openXC7#148: name the BEL-ambiguity check, the
split-half name-collision check, the BEL-inherit check, and the two
already-constrained checks instead of inlining them at the call site.

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

Copy link
Copy Markdown
Author

should be addressed...

@hansfbaier
hansfbaier merged commit 4dd779f into openXC7:stable-backports Aug 18, 2026
6 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.

2 participants