{"$schema":"https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json","document":{"category":"csaf_security_advisory","csaf_version":"2.1","distribution":{"tlp":{"label":"CLEAR"}},"lang":"en","notes":[{"category":"summary","text":"nvulnlookup v0.1.37's vendor-dashboard page handler (`/dashboards/<slug>`) hangs deterministically — the request never returns and the connection eventually times out at the proxy / client — for 12 of the 21 vendor slugs registered in `routes::vendor_dashboard::VENDOR_SPECS`.  Affected slugs (by source string): `csaf_cisco`, `csaf_microsoft`, `osv_golang`, `csaf_oracle`, `csaf_pentagrid`, `pysec`, `csaf_redhat`, `osv_rustsec`, `csaf_suse`, `osv_ubuntu`, `csaf_vmware`, `vulnrichment`.  Working slugs (control group): `cert-bund` (csaf_certbund), `fkie-nvd`, `gcve` (gcve_vl), `github`, `jvn` (jvndb), `ndaal` (csaf_ndaal), `opensuse` (csaf_opensuse).  The route shell returns HTTP 200 from `curl` for the working set in 1-2 s; the broken set never returns within a 12 s `gtimeout` budget across 3/3 attempts under low concurrent load.","title":"Summary"},{"category":"description","text":"Symptom: GET https://<host>:8080/dashboards/<broken-slug> hangs.  The handler enters a long-running iteration over the redb `SOURCE_INDEX` (or the underlying `INDEX` table) and never emits a Tokio yield or response chunk.  Browsers report a 'page took too long to respond' error; reverse-proxy clients (curl, headless Chrome with `--virtual-time-budget=4000`) hit their own timeouts; the kernel may eventually OOM-kill the vl-web process under sustained concurrent load (observed once in this same investigation: 12 GB RAM macOS host, NVD reseed + 30k-record csaf_opensuse import + concurrent headless-Chrome screenshot loop).\n\nReproduction:\n\n  $ for path in /dashboards/cisco /dashboards/microsoft \\\n                /dashboards/redhat /dashboards/cwe; do\n      code=$(curl -sk --max-time 6 -o /dev/null \\\n                  -w '%{http_code}/%{time_total}' \\\n                  \"https://localhost:8080${path}\")\n      printf '%-40s %s\\n' \"${path}\" \"${code}\"\n    done\n  /dashboards/cisco                        000/6.005\n  /dashboards/microsoft                    000/6.004\n  /dashboards/redhat                       000/6.005\n  /dashboards/cwe                          200/1.193\n\n3/3 attempts return HTTP 000 (curl --max-time fired) for cisco / microsoft / redhat consistently.  CWE returns 200 in ~1.2 s consistently.  No log line is emitted by vl-web during the hung interval (no INFO, no WARN, no panic).\n\nObserved root-cause hypothesis: `routes::vendor_dashboard::vendor_dashboard_page` calls `state.vuln_storage().get_last(&opts)` with `opts.source = Some(spec.source)` then `total_for_source(&state, spec.source)` which issues a second `get_last` with `number = 5_000`.  In `crates/vl-core/src/storage.rs::collect_source_index_ids`, the source filter is implemented as a full-table iteration that scans every key in `SOURCE_INDEX` and filters by source-string match.  For sources with NO data in the redb (the feeders for the broken slugs have not yet populated their share), the iteration walks the entire 192 000-record `SOURCE_INDEX` looking for matches and finds none — taking minutes on a 12 GB redb file.  For populated sources (cert-bund, github, ndaal, opensuse), the iteration short-circuits after 25 hits early in the scan and returns in milliseconds.\n\nThis hypothesis is consistent with the live observation that working/broken status correlates with feeder-populated vs feeder-pending state, and inconsistent with any explanation rooted in route registration (the routes ARE registered; the shell URL returns 200 in the working case).","title":"Description"},{"category":"details","text":"Affected handler chain:\n\n  vl-web/src/routes/vendor_dashboard.rs:419  page_handler_for(slug)\n  vl-web/src/routes/vendor_dashboard.rs:446  vendor_dashboard_page\n  vl-web/src/routes/vendor_dashboard.rs:463    state.vuln_storage().get_last(&opts)\n  vl-web/src/routes/vendor_dashboard.rs:479    total_for_source(&state, spec.source)\n  vl-web/src/routes/vendor_dashboard.rs:713      get_last(number=5000)\n  vl-core/src/storage.rs:633                   get_last(opts)  // branches on opts.source\n  vl-core/src/storage.rs:649                     Some(src) -> collect_source_index_ids\n\nAdditional impact paths:\n\n  * The HTMX partial endpoint /dashboards/<slug>/recent/partial uses\n    the same handler family (routes::vendor_dashboard::partial_handler_for).\n    The shell renders fine for some slugs because partial errors are\n    swallowed by HTMX, but the operator-perceived 'all dashboards\n    404' report from the v0.1.37 deployment was driven by the\n    partial-fetch failure pattern.\n  * Concurrent-load amplification: every additional concurrent\n    request to a broken slug ties up another redb read transaction\n    iterating the same 192k entries, eventually starving the\n    NVD/CSAF feeder writers of read-table slots.  Under sustained\n    load this manifests as the 'OOM-killed vl-web' crash observed in\n    this investigation.\n\nNo CVE has been requested for this advisory: the handler is reachable\nonly from authenticated/local listeners by default, returns no data\nexposure, has no privilege-escalation path, and is bounded by the\nfeeder population state.  The risk is operational (denial of\nservice / hung page render), not data-confidentiality.","title":"Affected components, paths, and operational impact"},{"category":"general","text":"Mitigation in v0.1.37 deployments (until the patched release lands):\n\n  1. Set a reverse-proxy timeout of 5-10 s on /dashboards/* so a hung\n     handler returns 504 to the client instead of stalling the\n     connection.\n  2. Add the affected slugs to the upstream UI navigation as\n     'feature-pending' until the patched build is shipped (the home\n     page navigation in v0.1.37 currently links to all 23 slugs\n     unconditionally).\n  3. When taking screenshots for documentation, skip the broken\n     slugs.  vulnerability-lookup-rs/scripts/capture_screenshots.sh\n     (committed in the same cycle) ships with a CAPTURE_SKIP_BROKEN\n     env var defaulting to 1 that skips the 12 known-broken slugs.\n     Set CAPTURE_SKIP_BROKEN=0 once the patched build is verified.\n  4. Avoid opening multiple browser tabs against broken slugs in\n     parallel — each tab spawns a 192 000-record redb scan.\n  5. Restart vl-web if it has been hit repeatedly with the broken\n     slugs and is showing memory pressure (Activity Monitor RSS\n     > 70 % of host RAM).\n\nFix in flight (separate commit in the same v0.1.37 cycle):\n\n  * Replace the full-table iteration in\n    `vl-core::storage::collect_source_index_ids` with a true\n    range-scoped iteration over the source-prefixed keys, OR add a\n    short-circuit guard that returns empty immediately when the\n    SOURCE_INDEX has no entries for that source.\n  * The fix is being investigated by a delegated agent track in the\n    v0.1.37 cycle; this advisory is the durable record so operators\n    have a referenceable 'why is X dashboard slow' document.","title":"Recommended action"},{"category":"other","text":"Disposition: PATCHED in v0.1.38.  Status: final.  The storage-layer fix landed in `crates/vl-core/src/storage.rs` (lazy iteration + count_source_index_entries short-circuit + get_top_vendors_for_source bounded scan), v0.1.38 was tagged and a fresh release built; verification on the running v0.1.38 binary confirms all 20 vendor dashboards return HTTP 200 within 0.05–2.84 s (no hang, no timeout, no OOM).  v0.1.37 deployments must upgrade to v0.1.38 to obtain the fix; the workarounds enumerated in the 'Recommended action' note above remain valid for sites that cannot upgrade immediately.","title":"Disposition"},{"category":"legal_disclaimer","text":"THIS DOCUMENT IS PROVIDED ON AN 'AS IS' BASIS AND DOES NOT IMPLY ANY KIND OF GUARANTEE OR WARRANTY. ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS INFORMATION INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS."}],"publisher":{"category":"vendor","contact_details":"security@ndaal.eu","issuing_authority":"ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG Security Team","name":"ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG","namespace":"https://ndaal.eu/csaf"},"references":[{"category":"self","summary":"This advisory in CSAF 2.1 format","url":"https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/raw/main/csaf/2026/035/ndaal-sa-2026-035.json"},{"category":"external","summary":"Affected handler factory: routes::vendor_dashboard::page_handler_for","url":"https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/crates/vl-web/src/routes/vendor_dashboard.rs#L419"},{"category":"external","summary":"Affected storage iteration: vl-core::storage::collect_source_index_ids","url":"https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/crates/vl-core/src/storage.rs#L633"},{"category":"external","summary":"VENDOR_SPECS slug -> source mapping","url":"https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/crates/vl-web/src/routes/vendor_dashboard.rs#L78"},{"category":"external","summary":"Companion v0.1.37 informational advisory ndaal-sa-2026-034 (dependency pin refresh)","url":"https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/raw/main/csaf/2026/034/ndaal-sa-2026-034.json"},{"category":"external","summary":"Mitigation script: vulnerability-lookup-rs/scripts/capture_screenshots.sh (CAPTURE_SKIP_BROKEN=1 default)","url":"https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/scripts/capture_screenshots.sh"}],"title":"ndaal Security Advisory: v0.1.37 vendor-dashboard handler hangs on empty-source SOURCE_INDEX scan — fixed in v0.1.38","tracking":{"current_release_date":"2026-04-29T08:15:00.000Z","generator":{"engine":{"name":"ndaal CSAF Generator","version":"1.0.0"}},"id":"ndaal-sa-2026-035","initial_release_date":"2026-04-28T13:30:00.000Z","revision_history":[{"date":"2026-04-28T13:30:00.000Z","number":"1.0.0","summary":"Initial advisory documenting that 12 of the 21 v0.1.37 vendor-dashboard slugs hang on the page handler.  Root-cause hypothesis: full-table iteration in collect_source_index_ids when the per-source key set is empty.  Status: interim; fix is in flight in the same v0.1.37 cycle and will land as a follow-up advisory.  Workaround documented (reverse-proxy timeout + nav 'feature-pending' + capture_screenshots.sh CAPTURE_SKIP_BROKEN=1)."},{"date":"2026-04-29T08:15:00.000Z","number":"2.0.0","summary":"Patch released in nvulnlookup v0.1.38: `vl-core::storage::collect_source_index_ids` now uses a range-scoped iteration over source-prefixed keys with a zero-entry short-circuit; `count_source_index_entries` and `get_top_vendors_for_source` use bounded scans.  Verification on the running v0.1.38 binary against a 12.9 GB redb (format v3): all 20 vendor dashboards return HTTP 200 in 0.05–2.84 s; the previous 12 hung-for-12-s slugs (cisco, microsoft, go, oracle, pentagrid, python, redhat, rust, suse, ubuntu, vmware, vulnrichment) all respond cleanly.  Status promoted from `interim` to `final`; product CSAFPID-0002 (v0.1.38) added to product_tree as the fixed product; vendor_fix remediation added pointing operators to v0.1.38."}],"status":"final","version":"2.0.0"}},"product_tree":{"branches":[{"category":"vendor","name":"ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG","branches":[{"category":"product_name","name":"nvulnlookup","branches":[{"category":"product_version","name":"0.1.37","product":{"name":"nvulnlookup 0.1.37 — vendor-dashboard handler hangs on empty-source SOURCE_INDEX iteration","product_id":"CSAFPID-0001"}},{"category":"product_version","name":"0.1.38","product":{"name":"nvulnlookup 0.1.38 — vendor-dashboard hang fixed (lazy iteration + bounded scan)","product_id":"CSAFPID-0002"}}]}]}]},"vulnerabilities":[{"notes":[{"category":"description","text":"The page handler at routes::vendor_dashboard::vendor_dashboard_page issues a get_last(opts) call with opts.source = Some(<source>) which dispatches to crates/vl-core/src/storage.rs::collect_source_index_ids.  When the redb SOURCE_INDEX table contains zero entries for the requested source string, the iterator walks the entire ~192 000-row table looking for a prefix match instead of using a range-scoped lookup, resulting in O(N) work per request.  Concurrent requests against the same broken slug compound the cost into O(N * R) and can exhaust read-transaction slots, eventually triggering kernel-OOM under tight memory budgets (12 GB observed)."}],"product_status":{"known_affected":["CSAFPID-0001"],"fixed":["CSAFPID-0002"]},"remediations":[{"category":"vendor_fix","details":"Upgrade to nvulnlookup v0.1.38 or later.  v0.1.38 replaces the full-table iteration in `vl-core::storage::collect_source_index_ids` with a true range-scoped iteration over the source-prefixed keys, plus a short-circuit guard that returns empty immediately when the SOURCE_INDEX has zero entries for the requested source.  Companion changes: `count_source_index_entries` short-circuits on empty source-set; `get_top_vendors_for_source` uses a bounded scan.  Verification on a 12.9 GB redb (format v3): all 20 dashboards return HTTP 200 in 0.05–2.84 s on the running v0.1.38 binary; the previous 12 hung-for-12-s slugs (cisco, microsoft, go [pysec], oracle, pentagrid, python [pysec], redhat, rust [osv_rustsec], suse, ubuntu, vmware, vulnrichment) all respond cleanly.","product_ids":["CSAFPID-0002"],"url":"https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/tree/main/release/v0.1.38"},{"category":"workaround","details":"(Pre-v0.1.38 deployments only.)  1) Apply a reverse-proxy timeout of 5-10 s to /dashboards/* so the connection is closed instead of stalling the worker.  2) Mark the 12 affected slugs as 'feature-pending' in the operator UI nav.  3) Use vulnerability-lookup-rs/scripts/capture_screenshots.sh with the default CAPTURE_SKIP_BROKEN=1 to avoid amplifying the hang during documentation builds.  4) Restart vl-web if memory pressure is observed after repeated hits to broken slugs.","product_ids":["CSAFPID-0001"]}],"metrics":[{"content":{"cvss_v3":{"version":"3.1","vectorString":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","baseScore":7.5,"baseSeverity":"HIGH","attackVector":"NETWORK","attackComplexity":"LOW","privilegesRequired":"NONE","userInteraction":"NONE","scope":"UNCHANGED","confidentialityImpact":"NONE","integrityImpact":"NONE","availabilityImpact":"HIGH"},"cvss_v4":{"version":"4.0","vectorString":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","baseScore":8.7,"baseSeverity":"HIGH","attackVector":"NETWORK","attackComplexity":"LOW","attackRequirements":"NONE","privilegesRequired":"NONE","userInteraction":"NONE","vulnConfidentialityImpact":"NONE","vulnIntegrityImpact":"NONE","vulnAvailabilityImpact":"HIGH","subConfidentialityImpact":"NONE","subIntegrityImpact":"NONE","subAvailabilityImpact":"NONE"}},"products":["CSAFPID-0001"]}],"title":"vendor_dashboard page handler hangs on empty-source SOURCE_INDEX scan"}]}