Thursday, October 22, 2026

If this page is useful, please consider donating a coffee

Wednesday, October 21, 2026

Security advisory: CVE-2026-105923 - Null-pointer dereference in QTriangulator reachable through VectorImageA denial-of-service vulnerability in the Qt Framework VectorImage QML type of the Qt Quick Vector Image module has been discovered and has been assigned the CVE id CVE-2026-105923. Affected versions: from Qt 6.8.0 to Qt 6.8.8, from Qt 6.9.0 to Qt 6.11.2 Impact: Processing specially crafted SVG content with the VectorImage QML type can cause the application to crash, resulting in loss of availability. CVSS 4.0 Score: 4.0 / MEDIUM Vector String: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:U/AU:N/R:U/RE:L/U:Green Mitigation: Do not load untrusted SVG files with the VectorImage QML type. Solution: Apply the following patch or update to Qt 6.8.9, Qt 6.11.3 or Qt 6.12.0 or later. Patches: dev: https://codereview.qt-project.org/c/qt%2Fqtbase/+/762279Qt Blog

Tuesday, October 20, 2026

Monday, October 19, 2026

No imageStatic Memory Planner for MLIR: A GSoC 2026 JourneyHi, I am Krish Gupta. Over the summer I worked with the LLVM Foundation as partof Google Summer of Code 2026 on building a static memory planner for MLIR , mentored by Matthias Springer and Javed Absar . This post walks through what webuilt, why it matters, and what the iterative process of landing it upstreamactually looked like. The Problem After bufferization, buffer lifetimes in MLIR IR are fully explicit: thecompiler knows exactly when each allocation starts and ends (for staticshapes; runtime-dependent allocations are handled conservatively). We use thisinformation to eliminate redundant heap allocations through a static memoryplanner. This matters for accelerator-oriented compilation. On many targets (embeddedCPUs, DSPs, custom accelerators) heap allocation is either not allowed orslow, because access to main memory is expensive. Even where heap allocationis available, individual small allocations hurt performance throughfragmentation and cache pressure. What you want instead is a single staticarena whose layout is computed at compile time: // Before: three separate heap allocations %a = memref .alloc() : memref 1024x f32 > // 4096 bytes %b = memref .alloc() : memref 512x f32 > // 2048 bytes memref .dealloc %a : memref 1024x f32 > memref .dealloc %b : memref 512x f32 > // After: one arena, two views %arena = memref .alloc() : memref 6144x i8 > %a = memref .view %arena[ 0 ][] : memref 6144x i8 > to memref 1024x f32 > %b = memref .view %arena[ 4096 ][]: memref 6144x i8 > to memref 512x f32 > // ... uses of %a and %b ... memref .dealloc %arena : memref 6144x i8 > The arena is memref so it can hold buffers of different datatypes. memref.view reinterprets slices back to their original types with zerooverhead. This is the core transformation the planner performs. How the Pass Works The pass is a FunctionOpInterface pass: it runs per function and processesthe entire function body in six steps: Step 1: Collect candidates: walk all memref.alloc ops, find their deallocs via alias analysis, apply eligibility checks.Step 2: Build descriptors: compute (size, alignment, timeStart, timeEnd) for each candidate using a single O(n+m) block scan.Step 3: Run the planner: feed descriptors into the chosen algorithm (trivial sequential packing or best-fit with lifetime overlap).Step 4: Assign offsets: compute total arena size, assign byte offsets to each candidate.Step 5: Create arena: allocate one memref (or use a passed-in arg).Step 6: Rewrite: replace each alloc with a memref.view into the arena, erase all associated deallocs. The core data structure is AllocationCandidate , which groups a memref.alloc , its set of potential deallocs (there can be more than one,reachable through alias chains), the computed byte offset, size, and alignment.The entire alias reasoning goes through BufferViewFlowAnalysis : a unifiedanalysis that models how buffers flow through arith.select , scf.if / scf.for results, cf branches, and view ops without anyop-type-specific code in the pass itself. The rest of the blog explains how wegot there. PR 1: The Foundation ( #205125 ) The first PR landed in late June and introduced the core pass: static-memory-planner-analysis . It handled the straightforward case (allocsand deallocs in the same basic block with a direct one-to-one relationship)and put in place the architecture everything else builds on: trivialMemoryPlanner : a pure C++ function that takes a list of (size, alignment, timeStart, timeEnd) descriptors and returns offsets. Beinga pure function means it can be unit tested independently and is easy to swap out. bestFitMemoryPlanner : a lifetime-aware algorithm that tries to reusearena slots for allocations whose lifetimes do not overlap. Arena modes : allocate (create the arena inside the function) and arg (the arena is passed as a function argument, useful for external memorymanagement). Alignment : the arena alignment is the LCM of all individual alignments,ensuring every view is correctly aligned regardless of datatype. The review process surfaced an important point early: upstream contributionculture expects every design decision to be justified and every edge caseexplicitly tested. The javedabsar1 feedback on adding timeStart / timeEnd fields even before they were wired up was a preview of what was coming. PR 2: arith.select Deallocs ( #209106 ) After the first PR landed, we hit the first real-world blocker. The ownership-based-buffer-deallocation pipeline routinely produces patterns likethis: %a = memref .alloc() : memref 1024x f32 > %b = memref .alloc() : memref 1024x f32 > %sel = arith.select %cond, %a, %b : memref 1024x f32 > memref .dealloc %sel : memref 1024x f32 > Here neither %a nor %b has a direct memref.dealloc user. The originalpass just incremented a skip counter for both and moved on. The fix was aforward DFS over the use-def graph, following BufferViewFlowOpInterface (which arith.select implements) to find reachable deallocs. This also required a reverse-alias safety guard : since dealloc %sel mayfree either %a or %b , erasing it after replacing both with arena views isonly safe if all allocs it might free are managed by the arena. If any is not,the dealloc cannot be erased. The review cycle on this PR shaped the finalapproach: drop the group-constraint fixpoint in favour of the guard, emit harderrors (not silent skips) for missing deallocs, and improve the buildAllocInfos scan from O(n x m) to O(n+m) using a single upfront DenseMap . PR 3: scf.if and the Architectural Shift ( #213634 ) The deallocation pipeline also produces structured-control-flow patterns thatthe DFS could not follow: // Pattern 1: dealloc on the scf.if result %a = memref .alloc() : memref 1024x f32 > %0 = scf.if %c -> memref 1024x f32 > { scf.yield %a } else { scf.yield %b } memref .dealloc %0 // dealloc is on %0, not %a // Pattern 2: dealloc inside the scf.if body %a = memref .alloc() : memref 1024x f32 > scf.if %c { memref .dealloc %a } The mentor pointed to BufferViewFlowAnalysis ( Transforms/BufferViewFlowAnalysis.h ), a unified alias analysis already usedelsewhere in the bufferization pipeline. It models buffer flow through allrelevant op types in one graph: BufferViewFlowOpInterface for arith.select , RegionBranchOpInterface for scf.if / scf.for , BranchOpInterface for cf branches, and ViewLikeOpInterface for memref.view . One analysis covers all cases. We deleted the hand-rolled DFS entirely and replaced it with two calls: analysis.resolve(alloc) gives the forward alias set: every SSA value the allocmay flow into. Walking users of each alias finds all potential deallocs. analysis.resolveReverse(dealloc.getMemref()) gives the reverse alias set: everyalloc that flows into the dealloc’s operand. Used by the safety guard. Two design decisions made this correct: Lifetime anchoring via findAncestorOpInBlock . A dealloc nested inside an scf.if body is in a different block. Block::findAncestorOpInBlock walks upthe parent chain and returns the enclosing scf.if op in the plan block. Thatop’s index becomes timeEnd (conservative, but correct). The buffer isconsidered live until the entire scf.if completes. The reverse-alias guard (again, but stronger). For pattern 2 above, thedealloc inside the scf.if body is attributed to %a and correctly anchored.For the scf.if-sharing-a-nested-alloc case: %a = memref .alloc() // entry block %0 = scf.if %c -> memref 1024x f32 > { memref .dealloc %a %b = memref .alloc() // NESTED -- not arena-managed scf.yield %b } else { scf.yield %a } memref .dealloc %0 // may free EITHER %a OR %b resolveReverse(%0) traces back to both %a and %b . Since %b is nested(not in the plan block), the guard fires: %a is conservatively skipped. Nomiscompile, no leak. The else vs then branch distinction turned out to be irrelevant to theanalysis: it works on SSA alias sets, not branch structure. PR 4: scf.for Test Cases ( #215221 ) After the BufferViewFlowAnalysis switch, scf.for patterns were handledcorrectly with no additional pass changes. This PR documented the results: Allocs nested inside an scf.for body are skipped (same rule as nested scf.if ). scf.for with iter_args passing buffers across iterations hits the guardin all three mentor patterns: the per-iteration nested alloc makes erasingany shared dealloc unsafe. The canonical valid case (entry-block allocs used inside the loop body withdeallocs in the entry block) transforms cleanly. The loop body gets arenaviews instead of raw alloc pointers. This PR was deliberately tests-only. Having a clear separation between “thiswas a correct transformation that just needed to be documented” and “thisrequired a new design decision” kept the review simple. PR 5: Edge and Error Cases ( #216610 ) The final PR stress-tested the pass with patterns existing tests did not coverand documented the results. Eight new analysis tests and four new error tests: Static and dynamic shapes coexisting in one function (static transforms,dynamic is skipped without interference). Both branches of an scf.if deallocating the same alloc (duplicate deallocops in the same arena slot, correctly deduplicated). Dealloc at depth 3: findAncestorOpInBlock walks up any depth, not just one level. Multi-hop alias chain: %a/%b -> %0 -> %1 -> dealloc . The resolve() BFSfollows the full chain. Cross-interface chain: arith.select -> scf.if -> dealloc . Two differentinterface implementations in one alias path, exercised by a single resolve() call. cf.cond_br to a sibling block rejected as unstructured control flow (sameerror as cf.br ). Walk-stops-on-first-error: WalkResult::interrupt() points the diagnostic atthe problematic alloc, not at subsequent valid ones. One test was removed during review: a dynamic alloc with no dealloc placed inthe errors file. It had no expected-error annotation since dynamic shapes aresilently skipped, not errored. A test without a defined diagnostic output doesnot belong in a -verify-diagnostics file. Lesson learned. Challenges The group constraint dead-end. The first implementation of arith.select support used a fixpoint iteration to enforce that all allocs sharing a deallocmust either all go into the arena or all be skipped. This was correct butcomplicated and fragile. Switching to the reverse-alias guard removed thefixpoint entirely: the guard is a simpler, more direct statement of the sameinvariant. Understanding what already exists. The biggest conceptual shift wasrealizing that BufferViewFlowAnalysis already did everything the hand-rolledDFS was trying to do, and more. The lesson: before implementing a bespoketraversal, read the existing analysis infrastructure. You will often find thatthe right tool exists and using it both simplifies your code and gives youcorrectness for free on patterns you had not thought about. Upstream review culture. Every error message, every test comment, everyvariable name is scrutinized. Rephrasing “unstructured control flow is notsupported” replaced a longer, less precise message. Swapping the order of twobranches in a test to make the alias chain more obvious. These details matterbecause the tests and messages are the documentation that future contributorswill read. What Was Built PR What landed #205125 Core pass, trivial + best-fit planners, alignment, arena modes #209106 arith.select dealloc chains, reverse-alias guard, O(n+m) lifetime scan #213634 BufferViewFlowAnalysis integration, scf.if support (nested deallocs + result aliases) #215221 scf.for test coverage, canonical loop case documented #216610 Edge and error case test suite (26 analysis tests, 4 error tests) The final pass lives in StaticMemoryPlannerAnalysis.cpp . Future Work The immediate open question is lifting the entry-block restriction: only allocsin the function’s entry block are planned today. Two concrete directions wediscussed: Path-sensitive lifetime tightening. The current [timeStart, timeEnd] interval model treats branches conservatively: a buffer freed inside an scf.if then-branch is considered live until the entire scf.if completes,even though the else path never held it. Adding dominance-based non-overlapdetection (using MLIR’s existing DominanceInfo ) would let mutually exclusivebranches share arena slots, unlocking patterns like: %0 = alloc() %1 = scf.if %c { %2 = alloc() // could share %0's slot -- they never co-exist dealloc(%0) yield %2 } else { yield %0 } dealloc(%1) Plugging in stronger planning algorithms. The planner function is pure andseparately testable, so dropping in a better algorithm is straightforward.Best-fit is already there. Beyond that, interval graph / graph-coloringapproaches and algorithms like minimalloc map directly onto the (size, alignment, timeStart, timeEnd) interface the planner already expects. Memory space awareness. Heterogeneous targets have multiple distinct memoryregions with different capacity constraints. Extending the planner to assignbuffers to specific memory spaces while respecting per-space limits is a naturalfollow-on. Acknowledgements Huge thanks to my mentors Matthias Springer and Javed Absar for the patient andprecise review feedback, for pointing at BufferViewFlowAnalysis at exactlythe right moment, and for treating every PR as a teaching opportunity. The LLVMcommunity’s review culture is demanding in the best possible way. The project GSoC page is at summerofcode.withgoogle.com/programs/2026/projects/XsjxBQ9o .Feel free to reach out with questions or to build on this work at krishgupta2832@gmail.com .The LLVM Project Blog
No imageReworking the prime path cacheI just reworked how the caching of prime paths works in zcov. Computing the prime paths can be quite expensive in both time and memory, so GCC limits to 250 000 paths by default. This limit was chosen because at around 250k the later passes slow down significantly (verifying the newly emitted instructions seem to be the worst offender, but I don’t have hard data here), but there are functions out there where just finding the prime paths is infeasible on my workstation. The prime paths are given by the structure of the function (specifically, the control flow graph), which makes it a prime candidate for caching. This is especially useful for zcov as it is an interactive application where every microsecond matters. Even more so since code tends to change very little between coverage runs (and most of those changes are in the test suite and not the code being tested), and so the same functions will be analyzed again and again. Since the same project will be tested over a long time with reasonably few and slow changes we have an perfect use case for a persistent cache. The caching feature was implemented a long time ago, and was quite quickly put together. It was for all intents and purposes the just-computed suffix tree dumped to disk. For non-trivial functions this was an obvious improvement, but still had a large space- and time overhead it meant storing all subpaths, too. I tried a few different things and put together a new bespoke representation for a set of prime paths optimized to reduce memory usage, which I then benchmarked on 485 functions from GNU Coreutils. In the benchmark I measured the time (in microseconds) it too to: Compute the prime paths Look up the prime paths in the old cache Look up the prime paths in the new cache Here are some aggregate statistics from a pandas dataframe: mean median min max path count 181698 15 1 84197299 compute µs 4636.26 6.00 1.00 1572546.00 new-cache-hit µs 44.79 7.00 4.00 13678.00 old-cache-hit µs 1716.59 10.00 5.00 525424.00 compute µs/path 0.53 0.45 0.01 1.73 new-cache µs/path 0.60 0.44 0.00 3.50 old-cache µs/path 0.86 0.64 0.00 3.00 What the median and min/max path count tells us is that most functions are small and simple, but once they go beyond that the path count explodes. That we apparently have higher throughput (lower elapsed per-path time) suggest that for very simple functions the cost of looking up the cache is higher than just computing the number of paths. This is confirmed by filtering out any function with less than 30 paths: mean median min max path count 530842 124 31 84197299 compute µs 13536.43 101.00 3.00 1572546.00 new-cache-hit µs 118.52 9.00 4.00 13678.00 old-cache-hit µs 4998.68 39.00 10.00 525424.00 compute µs/path 0.77 0.74 0.01 1.73 new-cache µs/path 0.08 0.06 0.00 0.35 old-cache µs/path 0.33 0.33 0.00 0.58 new-cache/compute 0.18 0.06 0.00 2.67 compute/new-cache 39.58 17.19 0.38 254.53 old-cache/compute 0.63 0.41 0.12 10.25 new-cache/old-cache 0.22 0.16 0.01 0.73 old-cache/new-cache 15.91 6.14 1.36 115.83 So on average the new cache takes 0.08µs/path while computing takes 0.77µs/path, a 10x on average as opposed to the approx 2x of the old cache. This is in practice skewed by the cost of the cache being high for very small functions (sub-50 paths tend to take less than 5 µs to compute). If we look at the fastest/slowest functions while still filtering out those with less than 30 paths: function path count compute µs new-cache-hit µs old-cache-hit µs compute µs/path new-cache µs/path three_arguments 31 13 7 15 0.42 0.23 main 31 5 11 15 0.16 0.35 value_to_baud 32 32 8 14 1.00 0.25 apply_mode 32 13 8 14 0.41 0.25 write_to_file 32 21 8 13 0.66 0.25 … … … … … … … string_to_baud 61 46 8 21 0.75 0.13 sane_mode 62 58 8 17 0.94 0.13 set_suffix_length 64 97 8 32 1.52 0.12 parse_symbols 64 55 7 20 0.86 0.11 set_file_security_ctx 65 45 7 23 0.69 0.11 … … … … … … … main 63790 24282 144 15101 0.38 0.00 lines_rr 74899 22539 100 4256 0.30 0.00 ng_pollard_rho2 1416562 101036 627 39961 0.07 0.00 ine_bytes_split 1425449 147657 633 27143 0.10 0.00 parents_private 84197299 1572546 13678 525424 0.02 0.00 From this table we can see that even for very complex functions the cache hits are fast; we fetch 84 million paths in about 13.5ms which is decently quick and should be fine. For comparison, computing the same function takes about 1.6s. And finally, the relative improvement: function path count compute/new-cache old-cache/new-cache three_arguments 31 1.86 2.14 main 31 0.45 1.36 value_to_baud 32 4.00 1.75 apply_mode 32 1.62 1.75 write_to_file 32 2.62 1.62 … … … … string_to_baud 61 5.75 2.62 sane_mode 62 7.25 2.12 set_suffix_length 64 12.12 4.00 parse_symbols 64 7.86 2.86 set_file_security_ctx 65 6.43 3.29 … … … … main 63790 168.62 104.87 lines_rr 74899 225.39 42.56 ng_pollard_rho2 1416562 161.14 63.73 ine_bytes_split 1425449 233.27 42.88 parents_private 84197299 114.97 38.41 As the number of path goes past 40–50 into moderately complex functions we are looking at from 5–10 speed improvement over computing and 2–4x speed improvement over the old cache. For complex functions, more than 50 000 paths, the new cache is more than 100x faster than computing, and more than 40 times faster than the old cache. One of the ways this has been made quick is by significantly compressing the representation, which also reduces memory pressure. The old cache needs roughly 460 MB to store all the prime paths, while the new cache needs 93 MB to store the same set, about 20%. Speed matters. This translates into making the experience using zcov much nicer and smoother.patch – Blog

Sunday, October 18, 2026

Saturday, October 17, 2026

Friday, October 16, 2026