Security review of xclbinutil - #9968
Merged
Merged
Conversation
…ction parsers
#### Problem solved by the commit
Five section parsers passed fixed-size embedded char/uint8_t arrays from
wire-format structs directly to boost::format("%s"), which calls strlen
internally. If a crafted xclbin fills the last array entry's string field
with no null terminator, strlen reads past the end of the heap-allocated
section buffer into adjacent memory.
Affected fields:
- ip_data::m_name (uint8_t[64]) in SectionIPLayout.cxx
- clock_freq::m_name (char[128]) in SectionClockFrequencyTopology.cxx
- mem_data::m_tag (unsigned char[16]) in SectionMemTopology.cxx
- mem_data::m_tag (unsigned char[16]) in SectionGroupTopology.cxx
- debug_ip_data::m_name (char[128]) in SectionDebugIPLayout.cxx
#### How problem was solved
Added XclBinUtilities::bounded_fixed_cstr<N>() template helper that
constructs a std::string from a fixed-size array using strnlen to
bound the read to N bytes. Applied at all primary put() call sites
and TRACE logging sites in all five parsers.
#### Risks (if any) associated the changes in the commit
Low. For well-formed xclbins the output is identical. For malformed
inputs, the string is now truncated at the field boundary rather than
reading past the allocation.
#### What has been tested and how
Built xclbinutil successfully.
#### Documentation impact (if any)
None
Signed-off-by: Soren Soe <2106410+stsoe@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ionAIEPartition
#### Problem solved by the commit
SectionAIEPartition::writeAIEPartitionImage() delegated to helper
functions (populate_partition_info, populate_pre_cdo_groups,
populate_cdo_groups, populate_PDIs, write_pdi_image) that all used
wire-format offset and size fields from the xclbin without any bounds
checking against bufferSize. An attacker-controlled xclbin could set any
of these fields to cause heap OOB reads:
- aie_partition::aie_pdi.offset/size (array of aie_pdi structs)
- aie_partition_info::start_columns.offset/size (uint16_t array)
- aie_pdi::cdo_groups.offset/size (array of cdo_group structs)
- aie_pdi::pdi_image.offset/size (PDI image data, also written to disk)
- cdo_group::mpo_name (string offset, missing bounded_mpo_cstr)
- cdo_group::dpu_kernel_ids.offset/size (uint64_t array)
- cdo_group::pre_cdo_groups.offset/size (uint64_t array)
#### How problem was solved
Added bufferSize parameter to all helper functions and added
overflow-safe bounds checks before forming any pointer from a
wire-format offset+count pair:
if (count > (bufferSize - offset) / sizeof(T))
throw std::runtime_error(...)
Replaced raw pBase+mpo_name with bounded_mpo_cstr() for the CDO group
name. pdi_image validated in populate_PDIs before write_pdi_image is
called.
#### Risks (if any) associated the changes in the commit
Low. Rejects malformed xclbins that would previously cause heap OOB
reads or write attacker-controlled data to PDI output files.
#### What has been tested and how
Built xclbinutil successfully.
#### Documentation impact (if any)
None
Signed-off-by: Soren Soe <2106410+stsoe@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…parsers #### Problem solved by the commit Three integer arithmetic bugs where wire-format-controlled values were used in calculations that could overflow or underflow before a bounds comparison: 1. SectionMCS.cxx: m_chunk::m_offset (uint64_t) was added to a pointer before comparison. Pointer wraparound on x86-64 caused the bounds check to pass for attacker-supplied offsets near UINT64_MAX, enabling heap OOB reads and writes. 2. SectionDNACertificate.cxx: paddingSize was computed as (_sectionSize - signatureSizeBytes) - paddingOffset, which underflows to ~2^64 when paddingOffset > sectionSize - signatureSizeBytes. The guard at line 95 only checked dnaEntriesBitSize/8 <= _sectionSize but did not account for signature overhead, allowing the subsequent binaryBufferToHexString call to read ~2^64 bytes from the heap. 3. XclBinUtilities.cxx: seekg positions were computed as signatureOffset + signature.signedByOffset/signatureOffset using unsigned int arithmetic, which wraps to zero for large values before being widened to std::streamoff. Also added missing gcount() checks. Additionally, the MBG tag compression loop accessed memIndexVector[idx+1] without a bounds guard, causing OOB read on the last element. #### How problem was solved - SectionMCS: check m_offset and m_size directly in offset-space against _sectionSize before forming the pointer. - SectionDNACertificate: tighten the guard to include signatureSizeBytes + sizeof(uint64_t) overhead; add explicit paddingOffset bounds check before subtraction. - XclBinUtilities seekg: cast signatureOffset to uint64_t before adding. - XclBinUtilities MBG loop: add idx+1 < size guard to inner while. #### Risks (if any) associated the changes in the commit Low. All changes tighten input validation; behavior for well-formed inputs is unchanged. #### What has been tested and how Built xclbinutil successfully. #### Documentation impact (if any) None Signed-off-by: Soren Soe <2106410+stsoe@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
…nBinary #### Problem solved by the commit Section.cxx compared m_sectionSize (uint64_t) against UINT64_MAX — a tautologically false check since a uint64_t can never exceed UINT64_MAX. The check was intended to guard the subsequent cast to unsigned int (m_bufferSize), but because it could never fire, any xclbin section with m_sectionSize > UINT32_MAX was silently truncated: the 4-GiB+ value wrapped to a small number, a tiny buffer was allocated, and all downstream consumers used the wrong buffer size without error. The same dead-code pattern appeared in the JSON image path at line ~420. #### How problem was solved Replace `> UINT64_MAX` with `> UINT32_MAX` in both guard locations. This correctly rejects any section whose size exceeds the unsigned int representation used for m_bufferSize. #### Risks (if any) associated the changes in the commit Low. Sections larger than 4 GiB are not valid in practice and would have been silently corrupted before. The new check throws explicitly instead of silently truncating. #### What has been tested and how Built xclbinutil successfully. #### Documentation impact (if any) None Signed-off-by: Soren Soe <2106410+stsoe@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
…d stream reads #### Problem solved by the commit Two logic/robustness bugs: 1. XclBinClass.cxx: ptInterfaces[0] was accessed unconditionally after as_vector() without checking whether the vector was empty. A crafted xclbin with partition_metadata that has no "interfaces" entry, or with a non-PR mode xclbin (where the size > 1 check is skipped entirely), caused undefined behavior via std::vector::operator[] on an empty vector — likely a crash. 2. XclBinUtilities.cxx: istream::read() of the SignatureHeader struct had no gcount() check. A truncated file would leave the signature struct zero-initialized; the signedBySize/signatureSize fields would be zero, silently skipping the data reads and masking the corruption. #### How problem was solved 1. Added `if (ptInterfaces.empty()) return;` guard before ptInterfaces[0]. 2. Added gcount() check after the SignatureHeader read, consistent with the existing checks on the signedBy and signature data reads (which were added as part of the Cat 3 fix). #### Risks (if any) associated the changes in the commit Low. The empty-check early return is safe — no interface UUID update occurs for xclbins with no interfaces node, which was already the behavior for well-formed xclbins. The gcount() check rejects truncated signature files that would previously have been silently misprocessed. #### What has been tested and how Built xclbinutil successfully. #### Documentation impact (if any) None Signed-off-by: Soren Soe <2106410+stsoe@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
…lities #### Problem solved by the commit The Boost < 1.64 fallback path in XclBinUtilities::exec() built a shell command line by joining the cmd path and args vector with spaces and passing the result directly to popen(). A user-controlled argument such as an elfLibrary path (--add-pskernel) containing shell metacharacters (e.g. '; rm -rf /') would be executed by the shell. The Boost >= 1.64 path (boost::process::child) passes arguments as a vector, bypassing the shell, so no injection is possible there. #### How problem was solved Added shell_quote() helper that wraps each argument in single quotes and escapes any embedded single quotes as '"'"'. Applied to both the cmd path and every element of args when building the cmdLine string for popen(). This prevents shell interpretation of any characters in user-controlled values regardless of their content. #### Risks (if any) associated the changes in the commit Low. The popen path is only compiled on systems with Boost < 1.64 (pre-2016). The quoting is conservative — paths with spaces or special characters that previously failed will now work correctly. #### What has been tested and how Built xclbinutil successfully. #### Documentation impact (if any) None Signed-off-by: Soren Soe <2106410+stsoe@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Soren Soe <2106410+stsoe@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
xfreid
approved these changes
Aug 6, 2026
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.
Problem solved by the commit
A thorough manual security review of
src/runtime_src/tools/xclbinutilidentified 17 vulnerabilities across 10 files where wire-format fields from attacker-controlled xclbin files were used without adequate bounds checking. The fixes are grouped into six categories below.Bug / issue (if any) fixed, which PR introduced the bug, how it was discovered
All bugs are pre-existing. Discovered by manual security review following CodeQL alert triage (alerts #129–#156). None of the bugs below were previously flagged by CodeQL.
How problem was solved, alternative solutions (if any) and why they were rejected
Category 1 — CWE-125: Unterminated fixed-size string fields (5 bugs)
Five parsers passed fixed-size embedded char/uint8_t arrays from wire-format structs directly to
boost::format("%s"), which callsstrlenand reads to the first null byte. A crafted xclbin with no null terminator in the last entry's string field causes a heap OOB read past the section buffer.Affected:
ip_data::m_name(SectionIPLayout),clock_freq::m_name(SectionClockFrequencyTopology),mem_data::m_tag(SectionMemTopology, SectionGroupTopology),debug_ip_data::m_name(SectionDebugIPLayout).Fix: Added
XclBinUtilities::bounded_fixed_cstr<N>()template that usesstrnlento bound the read to the field width. Applied at allput()andTRACEcall sites in all five parsers.Category 2 — CWE-125: Unchecked wire-format array offsets in SectionAIEPartition (7 bugs)
writeAIEPartitionImage()hadbufferSizeavailable but never passed it to any helper function, leaving all wire-formatoffset/sizepointer arithmetic unguarded. Seven sites were affected including one where attacker-controlled data was written to a PDI output file on disk (information disclosure).Affected fields:
aie_pdi.offset/size,start_columns.offset/size,cdo_groups.offset/size,cdo_group::mpo_name,dpu_kernel_ids.offset/size,pre_cdo_groups.offset/size,pdi_image.offset/size.Fix: Added
bufferSizeparameter to all helper functions. Added overflow-safe bounds checks (count > (bufferSize - offset) / sizeof(T)) before every array pointer formation. Replaced rawmpo_namepointer arithmetic withbounded_mpo_cstr(). Bounds-check temporaries reused in array cast and loop bounds.Category 3 — CWE-190/191: Integer overflow and underflow (4 bugs)
SectionMCS.cxx:
m_chunk::m_offset(uint64_t) was added to pointer before the bounds check. Pointer wraparound on x86-64 caused the check to pass for values near UINT64_MAX. Fix: validate offset and size directly in offset-space before forming the pointer.SectionDNACertificate.cxx:
paddingSizecomputed as(sectionSize - signatureSizeBytes) - paddingOffsetunderflows to ~2^64 whenpaddingOffsetis large, because the guard did not account for the signature and length-field overhead. Fix: include overhead in the guard; add explicit underflow check before subtraction.XclBinUtilities.cxx (seekg):
signatureOffset + signedByOffsetcomputed asunsigned intbefore widening tostd::streamoff, wrapping the seek position to zero. Fix: cast touint64_tbefore adding. Also added missinggcount()checks on all three signature reads.XclBinUtilities.cxx (MBG loop): inner
whileaccessedmemIndexVector[idx + 1]with noidx + 1 < sizeguard, causing a one-past-the-end read on the last iteration. Fix: add bounds guard to thewhilecondition.Category 4 — CWE-190: Dead guard + silent integer truncation in Section.cxx (1 bug)
The guard
m_sectionSize > UINT64_MAXis tautologically false for auint64_t. The subsequent cast tounsigned intsilently truncated any section size above 4 GiB, allocating a tiny buffer while all downstream consumers saw the wrong size with no error. Same dead-code pattern in two locations. Fix: replace> UINT64_MAXwith> UINT32_MAX.Category 5 — Logic/robustness bugs (2 bugs)
XclBinClass.cxx:
ptInterfaces[0]was accessed unconditionally without checking whether the vector was empty, causing undefined behavior (crash) for xclbins with nointerfacesnode. Fix: addif (ptInterfaces.empty()) return;guard.XclBinUtilities.cxx:
istream::read()of theSignatureHeaderstruct had nogcount()check. A truncated file left the struct zero-initialized, silently skipping all data reads. Fix: addgcount()check consistent with the existing checks on the data reads.Category 6 — CWE-78: Command injection in legacy popen path (1 bug)
The Boost < 1.64 fallback in
exec()joinedcmdandargswith spaces and passed the result topopen(). A user-controlled argument (e.g.--add-pskernelpath) containing shell metacharacters could inject arbitrary shell commands. The Boost >= 1.64 path usingboost::process::childis not affected. Fix: addedshell_quote()helper that wraps each argument in single quotes and escapes embedded single quotes as'"'"', preventing shell interpretation of any argument content.Risks (if any) associated the changes in the commit
Low across all categories. All changes are input validation only; behavior for well-formed xclbin files is unchanged. Malformed files that previously caused silent heap reads, data corruption, or crashes now throw
std::runtime_error, which xclbinutil already handles at its top level.What has been tested and how, request additional testing if necessary
Built xclbinutil successfully for all six change sets. Recommend:
mpo_*offsets and array counts, and verifying xclbinutil throws rather than leaking heap data.--add-pskernelwith a path containing shell metacharacters on a Boost < 1.64 build and verifying no injection occurs.Documentation impact (if any)
None.