gui: fix two empty-container dereferences in ClockTree - #11219
Open
openroad-ci wants to merge 2 commits into
Open
gui: fix two empty-container dereferences in ClockTree#11219openroad-ci wants to merge 2 commits into
openroad-ci wants to merge 2 commits into
Conversation
added 2 commits
August 24, 2026 12:54
ClockTree's clock constructor computed net_ from *clock_->pins().begin() unconditionally. A virtual clock -- one created by create_clock with no object list -- has an empty pin set, so this dereferences the end iterator of an empty std::set. The consumer already expects a null net for that case (ClockWidget::populate skips a tree whose getNet() is null), and libstdc++ happens to deliver one: the read lands on _Rb_tree_header::_M_node_count, yields nullptr, and dbNetwork::staToDb's null guard swallows it. libc++'s __tree is 32 bytes rather than 48, so the same read returns a live, 8-byte-aligned end-node pointer instead. That reaches dbNetwork::staToDb as an untagged sta::Pin* and raises [ERROR ORD-2018] Pin is not ITerm or BTerm or modITerm. Any GUI build against libc++ therefore fails to save clock tree images for a design whose SDC declares a virtual clock; 64 of the SDC files in OpenROAD-flow-scripts do, so this breaks save_images.tcl in the finish stage for most designs there. Guard the dereference so net_ stays null, restoring the behaviour the libstdc++ builds got by accident. Add a gui regression test that opens the Clock Tree Viewer on a design with a virtual clock and saves a clock tree image, registered in both CMake and Bazel. It reports pass without exercising anything when the binary has no GUI compiled in, and fails on the unfixed code under --//:platform=gui. Reported-by: Jeff Ng <jeffng-or@users.noreply.github.com> Diagnosed-with: Lucas Yuki Imamura <LucasYuki@users.noreply.github.com> Fixes: The-OpenROAD-Project/OpenROAD-flow-scripts#4371 Signed-off-by: SombraSoft <jsombrio@precisioninno.com>
findPathTo has two dereferences of a possibly-empty container: const sta::Pin* root = drivers_.begin()->first; is the same pattern just fixed in the clock constructor -- a ClockTree with no driver yields an end iterator, and reading ->first from it is UB. search_pin = *connections.begin(); is reachable independently. pin_map is a std::map and connections comes from operator[], which default-constructs an empty set for a pin that is not in the mapping, so a search_pin outside this tree falls through the find(root) check and then dereferences the end iterator of that empty set. Return an empty path in the first case and stop the walk in the second. The single caller, ClockTreeView's path-to renderer in clockWidget.cpp, already handles an empty result by simply drawing nothing. Unlike the constructor fix this path is only reachable from the interactive "highlight path to" action rather than from save_images.tcl, so no test accompanies it; driving it needs GUI mouse interaction. Note for anyone tempted to rely on a sanitizer for this class of bug: no standard library hardening setting available to these builds catches it. Measured with the hermetic clang 22 / libc++ 22 toolchain, *set.begin() and map.begin()->first on an empty container trap at neither _LIBCPP_HARDENING_MODE_FAST, _EXTENSIVE nor _DEBUG, and libstdc++ does not trap with _GLIBCXX_ASSERTIONS either. Only _GLIBCXX_DEBUG catches it, which is ABI-breaking and applies solely to the CMake builds -- the ones that already happen to work. Reading an adjacent member of the enclosing object is also in-bounds as far as ASan is concerned, so it cannot flag this either. Signed-off-by: SombraSoft <jsombrio@precisioninno.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request fixes undefined behavior in the Clock Tree Viewer when handling virtual clocks (which have no pins) by checking if the clock's pins are empty before dereferencing. It also adds safety checks in ClockTree::findPathTo for empty drivers or connections, and introduces a regression test clocktree_virtual_clock to verify this behavior. Feedback on the changes suggests simplifying the ternary expressions in the Bazel BUILD file to direct, idiomatic boolean expressions using in and not in.
Comment on lines
+41
to
+42
| check_log = False if test_name in PASSFAIL_TESTS else True, | ||
| check_passfail = True if test_name in PASSFAIL_TESTS else False, |
Contributor
There was a problem hiding this comment.
These ternary expressions can be simplified to direct boolean expressions using in and not in operators, which is more idiomatic and readable in Starlark/Python.
Suggested change
| check_log = False if test_name in PASSFAIL_TESTS else True, | |
| check_passfail = True if test_name in PASSFAIL_TESTS else False, | |
| check_log = test_name not in PASSFAIL_TESTS, | |
| check_passfail = test_name in PASSFAIL_TESTS, |
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.
Fixes The-OpenROAD-Project/OpenROAD-flow-scripts#4371, reported by @jeffng-or and investigated by @LucasYuki.
Symptom
An OpenROAD GUI binary built with Bazel aborts in the ORFS
finishstage whileflow/scripts/save_images.tclpopulates the Clock Tree Viewer:A CMake-built binary from the same source is fine, which is what made this look like a build-system problem.
Root cause
ClockTree's clock constructor (src/gui/src/staGuiInterface.cpp:552) did:net_ = getNet(*clock_->pins().begin());STAGuiInterface::getClockTrees()builds aClockTreefor every clock in the SDC (staGuiInterface.cpp:1522), including virtual clocks. A virtual clock —create_clockwith no object list — has an emptypins(), so this dereferences the end iterator of an emptystd::set. That is UB.The consumer already anticipates a null net for that case:
and libstdc++ happens to supply one: its
_Rb_treeis 48 bytes, the read lands on_Rb_tree_header::_M_node_count(zero),getNetreceivesnullptr, anddbNetwork::staToDb's null guard (src/dbSta/src/dbNetwork.cc:3141) swallows it. So the CMake builds work by accident.libc++'s
__treeis 32 bytes, so the same read lands on the adjacentleaf_pins_member and returns a live, 8-byte-aligned end-node pointer.sta::Pin*is pointer-tagged in its low 3 bits (dbNetwork.cc:246-253); an untagged-but-non-null pointer is exactly thekNonecase that raises ORD-2018 atdbNetwork.cc:3154.Bazel builds against libc++ (hermetic-llvm, no
cxxstdlibconstraint), CMake against system gcc/libstdc++ — hence bazel-only, and fully deterministic rather than a heisenbug.Blast radius
64 SDC files under
flow/designs/in ORFS declare a barecreate_clock, so this breakssave_images.tclfor most designs there, not justasap7/riscv32i.Fix
Guard the dereference.
net_is alreadynullptrin the init list, so virtual clocks keep taking the existing skip inClockWidget::populate— identical to today's libstdc++ behaviour.Verification
Built
--//:platform=gui //:openroadand compared against the CMake binary at the same commit (20d2d5c16e), on Nangate45gcdplus one barecreate_clock:ORD-2018bazelisk test --//:platform=gui //src/gui/...andbazelisk test //src/gui/...both pass 4/4.Test
New
src/gui/test/clocktree_virtual_clock.tcl, registered in both CMake (GUI_PASSFAIL_TESTS, gated onQt5_FOUND AND BUILD_GUI) and Bazel (ALL_TESTS+PASSFAIL_TESTS). It opens the Clock Tree Viewer on a design with a virtual clock and asserts a clock tree image is written. Verified to fail on the unfixed code under--//:platform=gui, and to report pass without exercising anything when the binary has no GUI.There was previously no test that opened the Clock Tree Viewer at all, which is why this survived.
Not fixed here
The same issue log also shows
GUI-0010rewritingfinal_clocks.webptofinal_clocks.webp.png. That is an independent defect: theqt-bazelprebuilts don't ship Qt'sqwebpimage-format plugin, soQImageWriter::supportedImageFormats()has nowebpandUtils::fixImagePathappends.png. Addressed separately in The-OpenROAD-Project/qt_bazel_prebuilts.Second commit: the sibling site
ClockTree::findPathTohad the same shape and is now fixed in this PR too:drivers_.begin()->firston aClockTreewith no driver — identical to the constructor bug.search_pin = *connections.begin()— independently reachable.pin_mapis astd::mapandconnectionscomes fromoperator[], which default-constructs an empty set for a pin that isn't in the mapping, so asearch_pinoutside this tree falls through thefind(root)check and then dereferences that empty set's end iterator.The only caller (
clockWidget.cpp:97) already handles an empty result by drawing nothing. No test accompanies it: unlike the constructor, this path is reachable only from the interactive "highlight path to" action, which needs GUI mouse interaction to drive.Correction: no sanitizer or hardening mode catches this
An earlier revision of this description suggested enabling
_LIBCPP_HARDENING_MODE/_GLIBCXX_ASSERTIONSas prevention. I measured it, and that's wrong — none of them catch an empty-container dereference of this shape. With the hermetic clang 22 / libc++ 22 toolchain,*set.begin()andmap.begin()->firston an empty container trap at none of_LIBCPP_HARDENING_MODE_FAST,_EXTENSIVE, or_DEBUG:(The mode value confirms the flag was actually in effect, so this isn't a misconfigured probe. Default in these builds is
NONE= 2.)libstdc++ with
_GLIBCXX_ASSERTIONSdoesn't trap either. Only_GLIBCXX_DEBUGcatches it (state = past-the-end) — and that is ABI-breaking and applies solely to the CMake/libstdc++ builds, i.e. the ones that already happen to work. So it isn't a usable CI lever for the configuration that actually breaks.ASan can't help either, and not just incidentally: in the real failure the read lands on
leaf_pins_, an adjacent member of the samesta::Clockobject, which is a perfectly in-bounds read as far as ASan is concerned. (I couldn't execute an ASan build here — the hermetic toolchain in my environment is missing thelibclang_rt.asan*archives — but the mechanism doesn't depend on running it.)The practical prevention is therefore what this PR does: fix both sites, and add a test that actually opens the Clock Tree Viewer. Worth keeping in mind for future reviews that libc++'s
__treeiterators are unchecked at every hardening level.