libsolv is a dependency-resolution library used by dnf/libdnf, zypper/libzypp, and other RPM-based package managers. It serializes parsed repository metadata into a binary cache format (.solv files) to avoid re-parsing repository XML on every run. repowrite() (src/repowrite.c) rewrites such a cache from an in-memory Repo/Repodata, in two passes: pass 1 (collectneededcb) collects which directory ids are used, and pass 2 (collectdatacb / collectfilelistcb) emits the serialized data.
Root cause: in collectneededcb() (src/repowrite.c:614-683), for REPOKEYTYPEDIR / REPOKEYTYPEDIRNUMNUMARRAY / REPOKEYTYPEDIRSTRARRAY keys, the code executes cbdata->dirused[id] = 1; at line 651, where id = kv->id comes directly from the KeyValue produced while iterating repodata via repodatasearch(). dirused is allocated at src/repowrite.c:1525 as solvcalloc(dirpool->ndirs, sizeof(Id)), i.e. sized to the number of known directories. For solvable data that is stored incore, the eager decoder (src/reposolv.c) validates every id against a maximum via datareadidmax()/SOLVERRORIDRANGE (e.g. reposolv.c:252-263) before it is used. However, for data stored KEYSTORAGEVERTICALOFFSET (used for large per-package data such as SOLVABLEFILELIST / REPOKEYTYPEDIRSTRARRAY), the bytes are lazily paged in and decoded on demand by datafetch() (src/repopack.h:131-211), which reads the id with datareadid()/datareadideof() and performs no range check at all. If the underlying .solv file's vertical filelist section contains a directory id greater than or equal to dirpool->ndirs (e.g. from a torn/corrupted cache after an unclean shutdown, or a hand-crafted .solv file), that raw, unvalidated id flows into collectneededcb() and is used directly as an array index into dirused, producing an out-of-bounds write of the constant value 1 far outside the allocated buffer. The reporter observed kv->id = 65137234 against dirpool->ndirs = 2737 in a field crash, i.e. a write roughly 260 MB past the end of the buffer.
The same unvalidated id is read back (not just written) without a bounds check at repowrite.c:844, 856, 865, and 917 (id = cbdata->dirused[id];) during the second serialization pass -- these are additional out-of-bounds reads beyond what the reporter's report explicitly enumerated. Independently, repodatadir2str() (src/repodata.c:356-410) walks the directory pool via dirpoolcompid()/dirpoolparent() (src/dirpool.h:34-41, 81-85), both of which index dp->dirs[did] with no range check against dp->ndirs; this function is reachable from readers such as dumpsolv and repoquery -l, giving a second, independent OOB-read primitive on the same class of corrupted/malicious input.
Reporter: Dinko Georgiev (StorPool), reported via PSIRT ticket PSIRTSUPT-22561 on 2026-08-26. No upstream fix version or commit is available at time of this analysis (not yet fixed upstream as of libsolv master 1b580675; this analysis used a shallow/depth-1 clone, so the reporter's claim that the code has been 'unchanged since d9ebbcf1 (2018-09-29)' could NOT be independently verified -- flagged as unverifiable, not confirmed or denied). The reporter's claim relating this to CVE-2026-9149 (a 0.7.38 hardening of repoaddsolv robustness against corrupt files) is noted as reporter-supplied prior art; this analysis could not verify the relationship or the CVE's exact fix commit against git history in the shallow clone, and treats this new bug as a distinct, unpatched code path (the vertical/paged filelist decoder), not covered by that prior hardening.
Independent verification performed via sandboxed static source review (see PSIRTSUPT-22561 for full verification report); CVSS independently scored at 5.5 (MODERATE), matching the reporter's own DoS/availability-only framing rather than an inflated rating.