dfinity / dfinity/public-multidex

OhShii Labs — index of all 161 counted findings across rounds 1–18 (153 public, 8 private) — map, not new findings

Open
#29 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Motoko
Stars
14
Forks
6
PR merge metrics
No merged PRs in 30d

Description

The numbers, and how to check them in one minute

how to verify
findings in public issues 157 thirty titles carry a numeric count and sum to 154; #39 states its three in words — 153 across thirty issues
disclosed only privately 8 eight GHSA ids, listed below — one carries two items, and one details #11.4, already counted in the 157
what we count 165 157 + 8
delivered as comments, not counted 2 named immediately below — a comment has no title to add up
issues of ours whose title carries no number 3 this index, #39 — whose three are inside the 144 — and #53 (a pointer to a private advisory)
what we have actually reported 167

Separately: 21 advisories exist, and only 6 of them carry findings counted here. Fourteen are
mirrors of the round-1 and round-2 public issues, filed 2026-08-07 once the channel worked; counting
them would count 65 findings twice. A seventh, GHSA-qgvc-r8wq-hjq2, is the reproduction detail for
#11.4 — a finding already counted in the 144, so counting the advisory too would count it twice.

We report 167 and we count 165. The gap is deliberate, and it is the whole design of this index:
157 of the 165 can be reproduced by adding up our own issue titles without opening a single
thread.
A finding delivered in a comment cannot be, so we leave it out of the total rather than
publish a number you would have to take on trust. The two are these, and both are measured rather
than argued:

  • On #45 — seven test suites abort partway
    through under any UTF-8 locale, one of them on the success path of a value-conservation
    assertion. Measured on both paths in real runs against a local replica; the same suite completes
    and prints its summary under LANG=C. It is larger than the item it amends, and the item it
    amends is ours.
  • On #41 — the remedy #41.1 proposed was
    executed. A clean checkout builds once scripts/gen-did.sh runs, so the missing generator was the
    only blocker rather than the first of several.
  • In the appendix of #55twenty-three further items, verified to the same standard and delivered as slugged paragraphs, not numbered findings. They are outside the 165 by the same rule as the comments: the title counts four, and a reader can check four. Each paragraph opens with a bold slug (oracle split, rebuild loop, exits still say refused, …) so it can be cited by name; if any of them is triaged as a finding we will add it here with the number it gets, not before. Three items that pass re-found are already in #54 (#54.1, #54.4, #54.6) and are not repeated.

The 157 is 4+7+6+4+4+4+7+4 (round 1) + 2+5+5+5+4+4 (round 2) + 8+9+6+3+4+3+5+3+4+4+5+6+3+9+7+9+4 (rounds 3–18, the last term being round 18's second part).
The 3 in the rounds 3–18 term that has no numeral in its title is #39, whose title says "three instrument items"; a regex over the other twenty-nine titles returns 150, and adding those three gives 153. Either way you have reproduced this index without reading it.

A correction to this line, since it was wrong before today. Until this update it read "every one of our twenty-two issue titles carries its own count" while a later paragraph said twenty-four, and the true figure was twenty-six. Both were wrong and they disagreed with each other. The finding totals they accompanied — 125 and 132 — were correct throughout; it was the issue count, and the instruction for reproducing it, that were not.


Index of everything OhShii Labs has filed, so the total is countable without opening
twenty threads. Nothing here is new — it is a map.

Updated 2026-09-02. It now covers rounds 1 through 18 (both parts): 165 findings — 157 in thirty-one public
issues and 8 disclosed only through the private channel.
A previous version of this line said
three; it omitted GHSA-qgvc-r8wq-hjq2, which is the reproduction detail for #11.4 and the most
severe single item in this index. That omission was ours and it undercounted our own worst finding —
the same visibility failure we raised on #5,
committed by us against ourselves.

Fourteen further advisories exist and are deliberately not counted. On 2026-08-07 we mirrored
every round-1 and round-2 issue into the advisory channel once it worked, so GHSA-4rrq-5qxx-w7vw
GHSA-p7wj-j39g-xmj7 are duplicates of #4#11 and #23#28, not additional findings.
Counting them would double-count 65 findings. They are listed at the end so the channel's contents
reconcile against this document. Both numbers are given throughout because only the public 144 is
checkable from the issue titles; the private 6 you have and we cannot show. The per-round breakdown is in the new section at the
end; rounds 1 and 2 keep their original per-finding map below, unchanged.

Why this exists. Our issues are grouped by theme, so each one carries several distinct
findings while the title only said "1/8". That reads as one item per thread. It is easy to
undercount by a factor of four, and we did exactly that to ourselves before noticing. The titles
now carry their own counts; this is the index behind them.

What matters more than the count

The single most severe thing we have filed is #11.4, and no severity table in this document
can express it.
ai-connect.html minted an unrestricted, mis-displayed Internet Identity
delegation — valid for every canister on the IC, POSTed to a host taken from an unvalidated URL
fragment, with a consent display that need not match what was actually signed. All four structural
claims were confirmed on 2026-08-07; verification independently derived the exploit primitive we had
withheld; it is fixed. Your own words on it:

not capped by #play. Play money bounds what the protocol can lose; it bounds nothing about a
user's II delegation, which is their real identity credential and was unscoped here.

That is why it is named here rather than in the table below: the table rates impact under
#production
, and this one's impact does not depend on posture at all. Read the table as
undercounting by exactly this finding.

The single #production CRITICAL is #23.1 — the self-funding loop re-entering after an
ambiguous ledger reject. The #play-reachable ones we would fix first are #25.1 (the season
reset locks every verified player out of deposits, silently) and #24.1 (the arbitrageur's clip
exceeds the DEX's own cap, so its inventory cannot be closed).

The severity mix — now complete, and reconciled per finding

Until 2026-08-11 this section covered rounds 1–2 only — 65 of the 165 — and said we would not
extend it because rounds 3 onward state severity in a different format and we do not publish a tally
we cannot reconcile per finding.
We have now done that reconciliation, so the table below covers
all 165 counted findings. How it was built is stated after it, because a severity tally is only
worth what its method is worth.

Rounds 3 onward rate on two axes, [play:X / prod:Y]. The column below is the #production
axis, because that is the axis the rounds 1–2 table used and the only one the two are comparable on.

severity (#production) rounds 1–2 rounds 3–18 private all 165
CRITICAL 1 0 0 1
HIGH 14 10 0 24
MEDIUM 31 49 7 87
LOW-MEDIUM 0 7 0 7
LOW / INFO ~19 23 1 ~43
N/A — the path does not exist on #production 0 3 0 3
65 92 8 165

On the shipped #play posture the same 49 findings of rounds 3–10 rate far lower — 3 HIGH,
6 MEDIUM, 27 LOW, 12 INFO, and one item (#41.2) carries a #production-only rating. That gap is
the point of the two-axis format: most of what these rounds found is latent today and becomes real
when custody lands.

Two things this table still undercounts, and we would rather say so than let it be read as a
ceiling.
First, #11.4 — the unrestricted, mis-displayed II delegation — is rated under
#production like everything else, and your own reply says its impact is "not capped by #play"
because a user's identity credential is not play money. It sits in the HIGH row and the row does not
express it. Second, the 2 comment-delivered findings named at the top of this index are outside the
165 entirely, and one of them is larger than the item it amends.

Method, so the number can be checked rather than believed. For each of the ten issues in rounds
3–10 we extracted every (finding anchor, severity label) pair from the issue body and asserted
the count against the finding count declared in that issue's own title
— the same rule we apply to
your instruments. Eight of the ten reconciled automatically. The two that did not are resolved
here in the open rather than by loosening the needle until it agreed:

  • #39 numbers its findings 1., 2.,
    3. rather than #39.1#39.3, so an anchor-based extractor finds none. All three carry
    [play:LOW / prod:LOW], read directly.
  • #41.2 carries a single-axis label,
    [prod:LOW-MEDIUM], with no play: rating, so a two-axis pattern cannot match it.

Rounds 1–2 keep the numbers published on 2026-08-04, unchanged; their LOW/INFO figure was
approximate when first published and we have not silently sharpened it.

Why some of this is public and some is private — the sequence, with its timestamps

Every date below is GitHub's own immutable value and every quotation is verbatim. The channel
changed three times, each time for a stated reason, and we would rather set that out than let the
mix look arbitrary.

1 — There was no private channel, and SECURITY.md says so itself. The advisory link pointed at
dfinity/multidex, a private repository, so it 404'd for every external reporter. That file now
records the consequence in its own words:

This link previously pointed at dfinity/multidex, which is a private repository — so it 404'd
for every external reporter, and there was no working private channel at all. Two independent
review teams hit that wall in August 2026 and published in the open rather than sit on their
findings; one withheld a user-targeting exploit for want of somewhere to send it.

The Menese DeFi Team filed the first public issue at 2026-08-01T11:10:12Z (#2). We filed ours
forty minutes later, at 11:50:27Z (#4). We were the team that withheld the exploit: its
mechanics went nowhere until a channel existed.

2 — Public filing turned out to do something a private channel cannot. At
2026-08-02T20:09:44Z @andreij6 filed #17 — "The outlier-trim fix proposed in #3 and #9 leaves
the defect in place"
— correcting a remedy that the Menese team and we had both proposed, in
two separate reports. All three teams withdrew it. The maintainer's own reply on #6 records the
same shape: "One later correction to our fix, from a third reviewer." And on #9:
"withdrawn — by you, and correctly." None of that is reachable from a private thread.

3 — The maintainer replied in public, then told us the private channel worked. His first reply
landed at 2026-08-07T01:39:22Z, followed by nineteen across the three teams. On #25 he wrote:

the advisory link now points at a real channel on this mirror, so the next one does not have to be
public.

4 — We used it immediately, and then over-used it. At 2026-08-07T10:23:54Z we filed
GHSA-qgvc-r8wq-hjq2, the withheld account-takeover mechanics — the report that had had nowhere to
go. Then, between 10:25:32Z and 10:25:49Z, we mirrored all fourteen round-1 and round-2 issues
into advisories as well.

5 — Those fourteen have never moved. All fourteen are still state: triage, unpublished and
unacknowledged, and they duplicate 65 findings that were already public. Continuing that way would
have moved our reports out of the one place where the correction in step 2 happened.

So from round 4 onward the rule is: a live defect in shipped code goes to the advisory channel,
because SECURITY.md asks for that; everything else — negatives, instrument findings, corrections
to our own work, and readiness observations about a transition that has not happened — stays
public, and the public issue names the private item by GHSA id, the reason in your policy's words,
and its class without its surface. The fourteen mirrors are excluded from every count in this
document for the same reason: counting them would count 65 findings twice.

How to cite these

Use #issue.item — e.g. #6.1, #9.1, #27.2. This is not a scheme we invented: the
Menese DeFi Team already cites our findings that way in #12 (#6.1, #6.2, #6.3, #7.1,
#7.2, #9.1, #9.2, #9.3), and @andreij6 uses it in #15, #17 and #18. We are just making it
explicit so it stays unambiguous.

Priority timestamps

Round 1 opened 2026-08-01 11:50:27 UTC (#4); round 2 opened 2026-08-04 17:10:01 UTC (#23).
Those are GitHub's own immutable createdAt values, verifiable with
gh issue view <n> --repo dfinity/public-multidex --json createdAt. The titles of all fourteen
issues were edited on 2026-08-04 to add the finding counts — that edit moves updatedAt only;
every createdAt above is unchanged and we checked all fourteen before and after.


Round 1 — 2026-08-01

#4 — client-side integrity — third-party script, absent CSP, and a ledger verifier whose certificate check never runs (4 findings)

  • #4.1 — The app loads executable JavaScript from unpkg.com with no SRI (highest impact in this group)
  • #4.2 — The .ic-assets.json5 that ships declares no security_policy, so the deployed app serves no CSP
  • #4.3 — The in-browser ledger verifier's certificate check never actually runs — and the repo already diagnosed this in its own CLI
  • #4.4 — The ledger verifier fetches the IC root key from the host it is verifying

#5 — unvalidated input reaching permanent state, and cycle burn with no cross-caller ceiling (7 findings)

  • #5.1setUserPreferences persists an unbounded, unvalidated blob per free identity, with no eviction and no purge path
  • #5.2 — Bridge claim(asset : Text) writes a permanent ledger row for an unvalidated asset string before the rejection
  • #5.3createMarginPool(name : Text, …) stores an unbounded name; the 64-pool cap is per-principal
  • #5.4aiComplete has no prompt length bound, so per-call cycle cost is caller-controlled — with no global cap and no meaningful floor
  • #5.5 — There is no global cross-caller rate cap and no cycle-reserve floor on any ingress method
  • #5.6aiActionExecuted writes the map the aiComplete registration gate exists to protect
  • #5.7 — A trap in any synchronous heartbeat subtask stops all maintenance permanently

#6 — liquidation evasion, a de-lever trap, and release priority that is purchasable (6 findings)

  • #6.1 — One base unit of ICPUSD dust makes a short margin pool permanently un-liquidatable
  • #6.2 — The post-fill liquidation hook bypasses the stale-mark guard the batch path enforces
  • #6.3 — A short cannot be closed once health falls into the 1.15–1.25 band
  • #6.4 — A free, indefinitely renewable L4 quote shield: mmQuoteStamp is set at staging and never cleared on cancel
  • #6.5 — Release priority and shed immunity are purchasable with self-dealt volume — rank 2 costs about fifteen dollars
  • #6.6STAGED_CAP_PER_OWNER is keyed on the raw principal, but one account controls 65 of them

#7 — vault and insurance — mint/redemption asymmetries and a guard evaluated on the wrong quantity (4 findings)

  • #7.1 — Vault NAV subtracts insuranceOwedUsd, but LP redemption pays from gross holdings
  • #7.2stakeInsurance mints against cash-only pool value, ignoring the fund's receivable
  • #7.3vaultPricesStale tests a leg's value rather than its balance, so a held leg priced at zero is invisible to the mint guard
  • #7.4 — Three unbacked-credit endpoints lack the IS_PRODUCTION interlock every sibling has

#8 — privacy and scoping — three surfaces that contradict their own documented guarantees (4 findings)

  • #8.1archiveExecute returns other users' rows, and two documentation surfaces state the opposite
  • #8.2getEventsRange / getDepositWithdrawals republish the per-principal index that the owner-gate on getEventsForPrincipals was added to cl
  • #8.3 — An OQL aggregate over the public deposit tape reconstructs capitalUsd exactly, rejoining the username↔principal split
  • #8.4 — Two smaller items in the same area

#9 — the oracle input path and the archive shed (4 findings)

  • #9.1 — The 2.5% breaker is a per-update rate limit with no absolute anchor, and the cadence is attacker-controlled
  • #9.2geptorFetchAndSweep has no single-flight guard: concurrent fetches apply a stale price stamped fresh
  • #9.3 — Every staged order arms an 8-outcall fetch, with no freshness precondition and no global cap
  • #9.4 — The archive's L2 shed fires on queue depth alone, so a healthy-but-backlogged shipper destroys 50,000 events of "permanent" history

#10 — deploy pipeline, controller-key hygiene, and posture/documentation drift (7 findings)

  • #10.1awk program-text injection from a fixed-name /tmp file, on the mainnet deploy path
  • #10.2cold_start.sh retains the pattern-kill the repo documents as having killed the live fleet twice
  • #10.3 — A helper script pre-authorises any unsigned binary to read every icp-cli private key without a prompt
  • #10.4 — No build verification, no module-hash check, no reproducible build, and npm install rather than npm ci
  • #10.5 — The -e ic route bypasses the environment allowlist and still resolves to the live subnet canisters
  • #10.6 — Posture and documentation drift
  • #10.7tests/MatchingEngine.test.mo does not compile

#11 — the AI proxy, and one delegation-handshake issue held back for private disclosure (4 findings)

  • #11.1 — Open LLM proxy: platform rules ride in the user turn, and the abuse guard is a substring sniff of the model's own output
  • #11.2 — Indirect prompt injection via createMarginPool(name), bounded by the confirm card
  • #11.3 — Two robustness items on the outcall path
  • #11.4ai-connect.html minted an unrestricted, mis-displayed II delegation. Filed as
    withheld; no longer withheld.
    All four structural claims were confirmed on 2026-08-07,
    verification independently derived the exploit primitive we had held back, and it is fixed. The
    line above this one said "withheld pending a private channel" until 2026-08-10 — that was accurate
    when written and stale for three days after, which is the same visibility problem we raised on #5.

Round 2 — 2026-08-04 (commit-point and check-integrity)

#23 — the self-funding loop can re-enter after an ambiguous ledger reject (2 findings)

  • #23.1 — An ambiguous ledger reject keeps the internal debit, arms no interlock, and the 10-minute auto-fuel loop re-enters
  • #23.2 — The auto-fuel trigger extrapolates a 5-minute burn sample ×288 with no clamp and no absolute ICP budget

#24 — the arbitrageur canister trades against the venue with no shared invariants (5 findings)

  • #24.1 — The arb's per-tick clip is 2.05× the DEX's per-call cap, so its own inventory becomes unflattenable
  • #24.2 — The rich branch commits the import before the hedge exists, and the justifying liquidity can be withdrawn inside the await
  • #24.3extMarketSwap has no price bound, and the arb prices its hedge off a mark up to three round-trips stale
  • #24.4 — The arb prices both legs of one arbitrage off two different marks
  • #24.5ARB_HOURLY_CAP_USD is charged gross on both legs against a tumbling window, so an hour's budget dies in about 65 seconds

#25 — the season boundary clears one half of five paired ledgers (5 findings)

  • #25.1 — The reset re-arms the wrong allowance bucket, and every Google-verified player is locked out of deposits
  • #25.2 — The reset clears playReservedUnits while the Bridge's claimables survive in another canister
  • #25.3 — The unshipped-history precondition ignores accounts.journal, which the same message then discards
  • #25.4performWorldWipe clears _priceRefreshInFlight, a single-flight flag it does not own
  • #25.5 — After a season reset, the prior season's history answers {events = []; total = 0} — a success

#26 — the archive chain — one bad segment fails every read, and the segments nobody can fix are the ones nobody observes (5 findings)

  • #26.1 — The season detach drops sealed archives out of every funding path, so the "permanent" ledger drains until the IC deletes it
  • #26.2 — One unreachable segment fails every History and OQL read, for every caller
  • #26.3 — Blackholed segments are never re-observed, so the chain table shows a stale ok forever
  • #26.4 — An archive spawn is unrecorded across its own await, and the epoch-race cleanup swallows the principal
  • #26.5adminReplayStep has no single-flight guard and advances its cursor only after the await

#27 — load shedding refuses exits, the Bridge counts admissions behind the commit, and two oracle clocks measure the wrong thing (4 findings)

  • #27.1 — The load-shed inspect is caller-scoped, so it refuses shed users' exits — cancel, close, unstake, withdraw
  • #27.2 — The Bridge advances its admission counter behind the DEX commit, so a lost reply double-charges the lifetime allowance
  • #27.3 — The jump breaker's 30-second independence gate compares message-arrival clocks, not sample times
  • #27.4 — The XRC fallback anchor is aged from arrival, never from the minute XRC priced, and it re-stamps refPrice as now

#28 — a test that cannot fail, two latent items, and the method note (4 findings)

  • #28.1tests/test_market_walks_locked.sh can never fail
  • #28.2run_all.sh computes the failing-assertion count and then discards it
  • #28.3 — Two actor-level constants are missing transient, so no upgrade can retune them
  • #28.4subPendingQty clamps where non-negativity is a real invariant, while its twin fails closed

Total: 65 findings across 14 issues.

Where we have been corrected

A count is only meaningful net of what did not hold. Five of our round-1 clearances were corrected
by the other two teams and we accept all five — they are acknowledged at the top of #23 and are
listed here so this index is not read as a scoreboard:

  • #21 (@andreij6) retracts our #10.6 certification of scripts/lint-ratchet.sh. We
    reproduced his finding independently — moc emits : type error [M0096], and the gate's
    grep -qE ': error' cannot match it.
  • #17 (@andreij6) shows the remedy we proposed in #9.1 is insufficient. We confirmed this
    by execution and posted the run under #3 and #17.
  • #15 item 2 (@andreij6) corrects our #7 closing paragraph on settleNettedPair.
  • #18 Finding 25 (@andreij6) corrects our #10.1 claim about the resetExchange delete loop.
  • #3 item 2 (Menese DeFi Team) corrects a clearance of ours on verifyChain paging.

Two of our findings were also already public elsewhere and are not counted as ours alone:
the getEventsForPrincipals gather-and-sort is @andreij6's #18 Finding 26, and the
MatchingEngine.test.mo compile failure is in both our #10.7 and Menese's #2.


Rounds 3–9 — added 2026-08-10

Rounds 1 and 2 audited the canister by theme. Everything after audits something the canister
depends on rather than the canister itself, so the severity mix is deliberately lower: these
rounds are about instruments, build inputs and readiness, and several of them have a negative
as their headline.

round issue subject findings
1 #4#11 the canister, by theme — eight threads 40
2 #23#28 the canister again — commit points and check integrity 25
rounds 1–2 subtotal 65
3 #36 the repository's own gates 8
3 #37 the standalone tools and the verifier 9
3 #38 the test fleet 6
4 #39 the OQL engine — the authorization boundary held 3
5 #40 what determines the bytes — the lockfile was correct 4
6 #41 readiness for NNS sole control 3
7 #44 instruments that change meaning when custody lands 5
8 #45 the eighteen candidates our own triage suppressed 3
9 #46 the assistant loop — card, session, instructions 4
10 #47 the emergency mechanism, self-funding recovery, and the tamper-evident tape's vocabulary 4
11 #48 the fund-moving core — five financial-logic findings 5
12 #49 readiness for real custody, and a margin-limit bypass 6
13 #50 the read surface — an LP position valued at NAV but paid from holdings 3
14 #51 the quote shield that survives a protocol kill, and the instruments watching it 9
15 #52 a per-user order cap one public path does not enforce, and a byte ceiling measured in characters 7
18 #54 the season seal races its own gates, and what the 1.60 drop fixed, left, and reopened 9
18, part 2 #55 the verifier takes its origin from the host, the Bridge wipe never fires where it is deployed — plus twenty-three further items in its appendix, not counted (see below) 4
rounds 3–18 subtotal 92
all public findings 157
1 GHSA-qgvc-r8wq-hjq2 account takeover — the #11.4 mechanics, disclosed privately · high · already counted as #11.4 0
4 GHSA-5rcg-pp8j-7pfx availability/resource · medium 1
4 GHSA-458x-24rf-g96f availability/resource · medium 1
7 GHSA-c6g7-mf5q-mgfq resource · low 1
9 GHSA-6qpg-j5gj-5mg5 cross-identity confidentiality · consent-display divergence · medium 2
11 GHSA-3j44-w8hr-f8x4 LP round-trip extraction, conditioned on unsettled arrears vs the loan book · medium 1
17 GHSA-h888-fcww-xq25 shares mint 1:1 against a vault that still holds assets — reproduced, now on PocketIC against 1fff1d7 · medium 1
18 GHSA-63hp-3pxg-9mcc App Connect after f233d18: the targets clause cannot be honoured by Internet Identity, so the scope the page promises is false on every run · medium 1
everything filed 165

Every subtotal is the sum of the rows above it, and every per-issue number is the count in that
issue's own title, which is checkable without opening it. Round 1 is 4+7+6+4+4+4+7+4; round 2 is
2+5+5+5+4+4.

The eight disclosed only privately, across seven advisories — counted in the 165 and not in the 157.
A seventh advisory, GHSA-qgvc-r8wq-hjq2, carries the mechanics of #11.4; that finding is counted
in the 144 and the advisory adds nothing to the total.

class our severity state today
GHSA-qgvc-r8wq-hjq2 account takeover — the withheld #11.4 mechanics high triage since 2026-08-07 · confirmed and fixed in the public thread
GHSA-5rcg-pp8j-7pfx availability / resource medium triage · unacknowledged since 2026-08-09
GHSA-458x-24rf-g96f availability / resource medium triage · unacknowledged since 2026-08-09
GHSA-c6g7-mf5q-mgfq resource low triage · unacknowledged since 2026-08-10

The class column matters more than the severity word: three of the four move no funds, expose no
data and cross no authorization boundary — they are availability and resource items, which is why
they are medium and low. The fourth is not in that family, and it is the one named at the top of
this document.

Why this table says less than the others, and why that is the point. All three are live and
unfixed. We hold to the rule that a public artefact never names the surface of a live unfixed
finding, so we cannot tell you here what they are — only that two are rated medium by us and one
low, and that the severities are ours rather than a triage outcome, because there has not been one.

Two of them have sat in triage for a day and a half without acknowledgement, which is not a
complaint — it is the trade we accepted when we moved live defects to the private channel from round
4 onward, and it is worth stating plainly so the asymmetry is visible: a finding filed publicly is
countable and checkable by anyone; a finding filed privately is countable by us and verifiable by
nobody but you.
The public 144 is the number a reader can audit. The 165 requires trusting us for
seven of them, and you are the only party who can confirm or correct that.

They are findings and they took the same work; they are separated only because a reader cannot
verify them against a public title, not because they count for less.

Where the count is not the point. Two of these rounds report that something works: #39 found the
OQL authorization boundary intact after attacking it, and #40 found the lockfile complete and
correct — 175 of 175 hashes. #44 opens by saying your own withdrawal-path hazard analysis is better
than the round that examined it. Those are results, and they are why the totals in this table should
not be read as a severity ranking against rounds 1–2.

Corrections we have published against our own filed work, so they are countable too: a
comment on #40 (a toolchain component we
called unique and was not), on #41 (an
evidence line already filed in #36, not conceded), and on
#5 (two of that issue's findings had been
worked without our knowing, and we re-derived both from scratch).


Attribution in shipped code — rounds 7–16, measured at 1fff1d7

Added 2026-09-01, after the 1.60 hardening drop. This section records where our finding numbers are
cited in the shipped tree. It is an attribution record, and it is deliberately not a fix census:
a fix can ship with no comment naming the issue, so the absence of a citation proves nothing, and
the presence of one in tests/ alone can mean the subject was the test suite. #46.3 is the worked
instance — derivationOrigin is implemented at five sites in src/frontend/, three of them present
before the drop, and no #46.3 string appears in src/ at all. Read the column as a lower bound.

The derivation, so it can be re-run rather than trusted:

grep -rnoE '#[0-9]{1,2}\.[0-9]{1,2}' src/ tests/

At 1fff1d7: 226 occurrences, 71 distinct items, 45 files. Controls: #51.1 in main.mo → 2;
#99.9 anywhere → 0. Of the 71, 36 are rounds 7–16 items of ours. Two test files are named for
this work: tests/test_audit_2026_08_fixes.sh and tests/test_mm_shield_kill_exits.sh.

item cited in src/ cited in tests/ where the attribution is
#44.4 0 4 tests only
#45.1 0 2 tests only
#46.1 3 5 both
#46.2 4 3 both
#46.3 0 1 tests only
#46.4 1 3 both
#47.1 8 4 both
#47.2 7 4 both
#47.3 1 1 both
#47.4 4 3 both
#48.1 5 12 both
#48.2 1 4 both
#48.3 1 1 both
#48.4 5 4 both
#48.5 10 13 both
#49.1 1 2 both
#49.3 1 3 both
#49.4 1 0 src only
#49.5 3 0 src only
#49.6 1 0 src only
#50.1 2 5 both
#50.2 1 1 both
#50.3 1 2 both
#51.1 2 1 both
#51.2 1 1 both
#51.3 0 2 tests only
#51.5 0 2 tests only
#51.8 0 2 tests only
#51.9 0 3 tests only
#52.1 0 1 tests only
#52.2 2 2 both
#52.3 0 1 tests only
#52.4 7 6 both
#52.5 1 1 both
#52.6 4 2 both
#52.7 0 1 tests only
36 items — 23 both · 3 src only · 10 tests only

The ten tests only rows are not gaps by default: #44.4, #45.1, #51.3, #51.5, #51.8, #51.9,
#52.1, #52.3 and #52.7 were findings about the test suite, and a test file is where such a fix
lands. #46.3 is the one where the fix is in source and uncited. Establishing fix status per item
means reading the code per item, which is a separate pass and is not claimed here.

The heaviest: #48.5 at 23 sites, #48.1 at 17, #52.4 at 13, #47.1 at 12. The clearest single
instance, main.mo:3407: clearMmShield(d.owner, d.marketId); // #51.1: a killed intent must not go on shielding.

The advisory channel, reconciled

Twenty-two advisories exist under dfinity/public-multidex, all filed by us. They break down as:

count counted in the 165?
mirrors of the round-1 and round-2 public issues, filed 2026-08-07 once the channel worked 14 no — they duplicate #4#11 and #23#28
disclosed only privately 8 seven carry the 8 counted; the eighth details #11.4, counted in the 157
total advisories 22

Our own severity labels across all twenty-two: 3 critical, 11 high, 7 medium, 1 low. Those are
per-advisory — an advisory mirrors a whole issue, so its label reflects the most severe finding in
that bundle. The per-finding table near the top of this document counts differently and is not in
conflict with it; a bundle labelled critical contains one critical finding and several that are not.

All twenty-two are still in triage as a state. Since 2026-09-01 that sentence needs a second half:
six of the seven advisories carrying private-only findings received in-thread fix notifications that
day — GHSA-qgvc, GHSA-5rcg, GHSA-458x and GHSA-3j44 read directly, GHSA-c6g7 and GHSA-6qpg
by the maintainer's own reference to them in the public threads, and the revised SECURITY.md (aac2da9) says advisories
are mirrored into the public tracker once the fix ships, GHSA id retained. The one advisory with no
reply is GHSA-h888-fcww-xq25
, the LP zero-supply mint (#53): its load-bearing sites are
byte-identical between 60f75f6 and 1fff1d7. The paragraph that follows was written before those
replies and is kept as the record of the period it describes. We were
recording that as a fact about the channel rather than a complaint: every substantive exchange we
have had with you happened in a public issue, and the two most useful corrections of this audit —
@andreij6's #17, and your own reply on #11 deriving the exploit primitive we had withheld —
could not have happened privately.

Comments carrying corrections to our own filed work, so they are countable too:
#40 (a toolchain component we called unique
and was not), #41 (an evidence line already
filed in #36 and not conceded), #5 (two of
that issue's findings had been worked without our knowing),
#6 (the remedy to #6.2 is unpinned at the
one site where it is load-bearing), #39 (its
conclusion holds for a better reason than we gave, and two of its counts need their needle stated),
#42 and
#21 (additions to other reporters' findings,
credited to them).

We are happy to open PRs; several of these are one to a few lines.

— Ravenith, OhShii Labs

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the issue body's count tables and the referenced SECURITY.md disclosure note. Check that the per-round totals, advisory exclusions, and correction text agree with the linked issue list. Done means the index remains a verifiable map without adding new findings.

Written by the indexing model from the issue text.

Assessment

Domain
documentation
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.