<# .SYNOPSIS Builds a deliberately patched UO client for the Asset Bridge phase 0 spike. .DESCRIPTION docs/link/v8.md section 16 phase 0 drives ServUO's vendored `Ultima` decoders "over a deliberately patched client". Stock clients are not the interesting case: they are the case the library was written against, and the whole reason phase 0 exists is that section 4 chose to call code that can take the shard down if it is wrong. A shard operator's client is patched -- custom art, a verdata.mul, a hand-edited Bodyconv.def -- and that is what has to be survived. This copies a client and then breaks the copy in four deliberate, catalogued ways. It NEVER writes to the source: every file it patches is hashed before and after, and a changed source hash aborts the run. Each defect is recorded in `patched-client.manifest.json` next to the copy, so the probe's report can be read against what was actually done rather than against a memory of it. The manifest is the answer to "is a nonzero REFUSED-BUT-DECODED count a bug or the point?". .PARAMETER Source The client to copy. Defaults to this machine's. .PARAMETER Dest Where to build the patched copy. Needs ~3.5 GB. .PARAMETER Tiers Which defects to apply. Default: all four. verdata Author a verdata.mul, which this client does not have. Ultima consults Verdata on EVERY art and anim lookup (Art's FileIndex is built with verdata file id 4, Animations' with 6), so on a client with no verdata.mul that entire branch is dead code that has never been exercised -- the largest untested surface in the library we are about to depend on. Includes one legitimate patch and one whose lookup points past verdata.mul's own end, because `FileIndex.Seek` bounds-checks the mul and does not bounds-check verdata. customart Fill unused artidx.mul slots with real records appended to art.mul, the way a custom-art shard does. Tests that our out-of-range accounting comes from the file rather than from a constant someone wrote down. corrupt Rewrite index entries and record headers into the shapes that reading Art.cs says are reachable: a lookup past EOF, a record that starts inside the file and ends outside it, a length too small for a header, absurd dimensions, a row table pointing outside its own record, and a land tile shorter than the fixed 2,024 bytes LoadLand always reads. bodyconv Add Bodyconv.def lines pointing bodies at an anim file that holds nothing, and at an index in another file that holds something unrelated -- the gargoyle-666 spider case, reproduced on purpose. Proves the extractor takes BodyConverter.Convert's answer and stops (v8.md section 4.3). nouop Move artLegacyMUL.uop aside, so art is read from art.mul/artidx.mul. This is not cosmetic and it is not optional if you want the customart or corrupt tiers to mean anything. FileIndex's UOP constructor ends with a bare `MulPath = uopPath`: when artLegacyMUL.uop is present it wins OUTRIGHT and art.mul / artidx.mul are never opened. Every index-level defect below writes to files the library does not read on a modern client, so without this tier those two tiers are inert while still reporting that they applied. It is also a real configuration in its own right: plenty of shards run mul-only clients, and a custom-art shard that adds graphics to art.mul while the UOP is still there gets nothing at all -- an operator trap worth knowing about. .PARAMETER SkipCopy Re-patch an existing copy without re-copying 3.5 GB. Only safe on a copy this script made and has not patched yet -- patching twice compounds the defects and invalidates the manifest. .EXAMPLE .\tools\patch_client.ps1 -Dest D:\uo-patched-client .NOTES Test scaffolding. Never deployed. The copy contains EA's client art -- like every other extraction in this project it stays on the machine that made it and is never committed. #> [CmdletBinding()] param( [string] $Source = 'D:\Games\Electronic Arts\Ultima Online Classic', [Parameter(Mandatory = $true)] [string] $Dest, [ValidateSet('verdata', 'customart', 'corrupt', 'bodyconv', 'nouop')] [string[]] $Tiers = @('nouop', 'verdata', 'customart', 'corrupt', 'bodyconv'), [switch] $SkipCopy, [switch] $Force ) $ErrorActionPreference = 'Stop' # Files this script may write to in the copy. Anything not on this list is a bug in the script, # and the source-hash check at the end is what proves it. $PatchTargets = @('artidx.mul', 'art.mul', 'verdata.mul', 'Bodyconv.def', 'artLegacyMUL.uop') # -- Little-endian helpers (BitConverter is fine, but the intent reads better named) ---------- function Read-Int32LE([byte[]] $Bytes, [int] $Offset) { return [BitConverter]::ToInt32($Bytes, $Offset) } function Write-Int32LE([byte[]] $Bytes, [int] $Offset, [int] $Value) { [Array]::Copy([BitConverter]::GetBytes([int] $Value), 0, $Bytes, $Offset, 4) } function Get-ArtEntry([byte[]] $Idx, [int] $Index) { $at = $Index * 12 return [pscustomobject]@{ Index = $Index Lookup = Read-Int32LE $Idx $at Length = Read-Int32LE $Idx ($at + 4) Extra = Read-Int32LE $Idx ($at + 8) } } function Set-ArtEntry([byte[]] $Idx, [int] $Index, [int] $Lookup, [int] $Length, [int] $Extra) { $at = $Index * 12 Write-Int32LE $Idx $at $Lookup Write-Int32LE $Idx ($at + 4) $Length Write-Int32LE $Idx ($at + 8) $Extra } # The defect catalogue. Every mutation appends to this, and it is written out as the manifest. $script:Defects = New-Object System.Collections.ArrayList function Add-Defect([string] $Tier, [string] $Key, [string] $What, [string] $Expect) { [void] $script:Defects.Add([pscustomobject]@{ tier = $Tier key = $Key what = $What expect = $Expect }) Write-Host (" {0,-22} {1}" -f $Key, $What) } # -- Preflight -------------------------------------------------------------------------------- if (-not (Test-Path -LiteralPath $Source)) { throw "source client not found: $Source" } $sourceFull = (Resolve-Path -LiteralPath $Source).Path if (Test-Path -LiteralPath $Dest) { $destFull = (Resolve-Path -LiteralPath $Dest).Path if ($destFull -eq $sourceFull) { throw "Dest is the source client. Refusing -- this script destroys what it points at." } if (-not $SkipCopy -and -not $Force) { throw "$Dest already exists. Pass -Force to overwrite it, or -SkipCopy to patch it in place." } } Write-Host "source: $sourceFull" Write-Host "dest: $Dest" Write-Host "tiers: $($Tiers -join ', ')" Write-Host "" # Hash the source files we are about to touch, so "it never writes to the source" is checked and # not merely asserted. $before = @{} foreach ($name in $PatchTargets) { $path = Join-Path $sourceFull $name if (Test-Path -LiteralPath $path) { $before[$name] = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash } } # -- Copy ------------------------------------------------------------------------------------- if ($SkipCopy) { Write-Host "skipping copy (-SkipCopy)" if (-not (Test-Path -LiteralPath $Dest)) { throw "-SkipCopy but $Dest does not exist" } } else { Write-Host "copying (this is ~3.5 GB; a few minutes)..." # /MIR so a -Force re-run starts clean rather than merging into an already-patched tree. # /NJH /NJS /NDL /NFL keep robocopy's output to the errors. $null = robocopy $sourceFull $Dest /MIR /R:1 /W:1 /NJH /NJS /NDL /NFL /NP # Robocopy exit codes below 8 are success; 8 and above are real failures. if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit code $LASTEXITCODE" } # Robocopy's "1 = files were copied" would otherwise become this script's exit code and read # as a failure to anything checking it. $global:LASTEXITCODE = 0 Write-Host "copied." } Write-Host "" $destFull = (Resolve-Path -LiteralPath $Dest).Path $artIdxPath = Join-Path $destFull 'artidx.mul' $artMulPath = Join-Path $destFull 'art.mul' if (-not (Test-Path -LiteralPath $artIdxPath)) { throw "no artidx.mul in the copy" } $idx = [System.IO.File]::ReadAllBytes($artIdxPath) $entryCount = [int] ($idx.Length / 12) $artMulLength = (Get-Item -LiteralPath $artMulPath).Length Write-Host ("artidx.mul holds {0:N0} entries; art.mul is {1:N0} bytes" -f $entryCount, $artMulLength) Write-Host "" # -- Tier: nouop ------------------------------------------------------------------------------ $uopPath = Join-Path $destFull 'artLegacyMUL.uop' $uopPresent = Test-Path -LiteralPath $uopPath if ($Tiers -contains 'nouop') { Write-Host "tier nouop" if (-not $uopPresent) { Write-Host " no artLegacyMUL.uop in the copy -- already a mul-only client" } else { Move-Item -LiteralPath $uopPath -Destination "$uopPath.disabled" -Force $uopPresent = $false Add-Defect 'nouop' 'artLegacyMUL.uop' 'moved aside so art is read from art.mul/artidx.mul' ` 'every index-level defect below becomes reachable; without this they are inert' } Write-Host "" } elseif ($uopPresent -and (($Tiers -contains 'corrupt') -or ($Tiers -contains 'customart'))) { Write-Host " WARNING: artLegacyMUL.uop is present and the nouop tier was not selected." Write-Host " FileIndex prefers the UOP outright, so the corrupt and customart tiers" Write-Host " will write to files the library never opens. Add -Tiers nouop." Write-Host "" } # Static ids are offset by 0x4000 in the index; land tiles occupy 0..0x3FFF. $StaticBase = 0x4000 # Find donor records to copy and victims to corrupt: real, modestly sized statics, so the defects # are applied to entries that genuinely work today. Picking arbitrary ids risks landing on slots # that are already empty, where a "defect" would prove nothing. $donors = New-Object System.Collections.ArrayList for ($id = 0x1000; $id -lt 0x3000 -and $donors.Count -lt 24; $id++) { $e = Get-ArtEntry $idx ($id + $StaticBase) if ($e.Lookup -ge 0 -and $e.Length -gt 200 -and $e.Length -lt 4000 -and ($e.Lookup + $e.Length) -le $artMulLength) { [void] $donors.Add([pscustomobject]@{ Id = $id; Entry = $e }) } } if ($donors.Count -lt 12) { throw "found only $($donors.Count) usable donor statics -- the copy looks wrong" } Write-Host "using donor statics: $(($donors | Select-Object -First 12 | ForEach-Object { $_.Id }) -join ', ')" Write-Host "" $idxDirty = $false # -- Tier: customart -------------------------------------------------------------------------- if ($Tiers -contains 'customart') { Write-Host "tier customart" # A custom-art client does not fill spare slots -- artidx.mul is exactly sized (62,692 # entries here, not one to spare), so adding art means GROWING the index. `Art` builds its # FileIndex with length 0x10000, so there is room for 2,844 more ids before the library stops # looking, and the stock ceiling turns out to be nothing more than the size of a file. $idxCeiling = 0x10000 if ($entryCount -ge $idxCeiling) { Write-Host " artidx.mul is already at the 0x10000 ceiling -- skipping tier" } else { $addCount = 8 $grown = New-Object byte[] (($entryCount + $addCount) * 12) [Array]::Copy($idx, 0, $grown, 0, $idx.Length) $idx = $grown # Read every donor record BEFORE opening the append handle. Append mode takes an # exclusive lock, so reading the same file while appending to it fails outright. $buffers = @() $reader = [System.IO.File]::Open($artMulPath, 'Open', 'Read', 'ReadWrite') try { for ($n = 0; $n -lt $addCount; $n++) { $donor = $donors[$n] $buffer = New-Object byte[] $donor.Entry.Length [void] $reader.Seek($donor.Entry.Lookup, 'Begin') [void] $reader.Read($buffer, 0, $buffer.Length) $buffers += , $buffer } } finally { $reader.Dispose() } $appendAt = $artMulLength $stream = [System.IO.File]::Open($artMulPath, 'Append', 'Write') try { for ($n = 0; $n -lt $addCount; $n++) { $buffer = $buffers[$n] $stream.Write($buffer, 0, $buffer.Length) $slot = $entryCount + $n $newId = $slot - $StaticBase Set-ArtEntry $idx $slot $appendAt $buffer.Length $donors[$n].Entry.Extra $appendAt += $buffer.Length Add-Defect 'customart' "static/$newId" ` "custom art appended past the stock ceiling (a copy of static/$($donors[$n].Id))" ` 'decodes cleanly; proves the ceiling is read from the file, not from a constant' } } finally { $stream.Dispose() } $entryCount += $addCount $idxDirty = $true } Write-Host "" } # -- Tier: corrupt ---------------------------------------------------------------------------- if ($Tiers -contains 'corrupt') { Write-Host "tier corrupt" $artMulLength = (Get-Item -LiteralPath $artMulPath).Length $v = 8 # donors 0..7 may have been consumed by customart as sources; they are unmodified # 1. A lookup past the end of art.mul. FileIndex.Seek DOES check this one # (`Stream.Length < e.lookup`), so the library and the validator should agree. $victim = $donors[$v++].Id Set-ArtEntry $idx ($victim + $StaticBase) ([int]($artMulLength + 4096)) 512 0 Add-Defect 'corrupt' "static/$victim" 'lookup 4 KB past the end of art.mul' ` 'refused by the validator; Seek also catches this one, so no picture' # 2. A record that STARTS inside the file and ENDS outside it. This is the gap: Seek checks # the start and never the end, stream.Read returns short, the decoders ignore the count, # and m_StreamBuffer still holds the PREVIOUS asset. The expected outcome is a picture of # something else entirely, reported as a success by every count in the library. $victim = $donors[$v++].Id Set-ArtEntry $idx ($victim + $StaticBase) ([int]($artMulLength - 64)) 8192 0 Add-Defect 'corrupt' "static/$victim" 'record starts 64 bytes before EOF and declares 8,192' ` 'REFUSED BUT DECODED -- the stale-buffer wrong picture' # 3. A length too small to hold even the 8-byte header. $victim = $donors[$v++].Id $donorEntry = $donors[$v - 1].Entry Set-ArtEntry $idx ($victim + $StaticBase) $donorEntry.Lookup 4 0 Add-Defect 'corrupt' "static/$victim" 'declared length 4 -- smaller than the static header' ` 'refused by the validator' # 4/5/6 rewrite the record BODY, so they need their own bytes rather than an index edit. # Appended to art.mul and pointed at, which leaves the donor's real record intact. $stream = [System.IO.File]::Open($artMulPath, 'Append', 'Write') try { $appendAt = (Get-Item -LiteralPath $artMulPath).Length # 4. Absurd dimensions. LoadStatic allocates new Bitmap(width, height) straight from two # ushorts in the file. 8000x8000 is ~128 MB -- survivable, and the point is made; the # same field can ask for 65535x65535, which is 8 GB from a two-byte edit. $victim = $donors[$v++].Id $rec = New-Object byte[] 2048 [Array]::Copy([BitConverter]::GetBytes([uint16] 8000), 0, $rec, 4, 2) [Array]::Copy([BitConverter]::GetBytes([uint16] 8000), 0, $rec, 6, 2) $stream.Write($rec, 0, $rec.Length) Set-ArtEntry $idx ($victim + $StaticBase) $appendAt $rec.Length 0 $appendAt += $rec.Length Add-Defect 'corrupt' "static/$victim" 'header declares 8000x8000 (a ~128 MB allocation from two bytes)' ` 'refused by the validator; the library would allocate it' # 5. A row-lookup table pointing outside the record. This is what LoadStatic's unbounded # read cursor was written to walk off the end of. $victim = $donors[$v++].Id $rec = New-Object byte[] 512 [Array]::Copy([BitConverter]::GetBytes([uint16] 32), 0, $rec, 4, 2) # width [Array]::Copy([BitConverter]::GetBytes([uint16] 32), 0, $rec, 6, 2) # height for ($row = 0; $row -lt 32; $row++) { # Each row's offset is added to (height + 4); 60000 puts every row far outside. [Array]::Copy([BitConverter]::GetBytes([uint16] 60000), 0, $rec, (8 + $row * 2), 2) } $stream.Write($rec, 0, $rec.Length) Set-ArtEntry $idx ($victim + $StaticBase) $appendAt $rec.Length 0 $appendAt += $rec.Length Add-Defect 'corrupt' "static/$victim" 'row table points 60,000 words outside a 512-byte record' ` 'refused by the validator; the library reads adjacent heap' # 6. A well-formed row table whose run length overruns the record. $victim = $donors[$v++].Id $rec = New-Object byte[] 256 [Array]::Copy([BitConverter]::GetBytes([uint16] 16), 0, $rec, 4, 2) [Array]::Copy([BitConverter]::GetBytes([uint16] 2), 0, $rec, 6, 2) [Array]::Copy([BitConverter]::GetBytes([uint16] 0), 0, $rec, 8, 2) # row 0 offset [Array]::Copy([BitConverter]::GetBytes([uint16] 0), 0, $rec, 10, 2) # row 1 offset $runAt = (2 + 4) * 2 # (height + 4) words [Array]::Copy([BitConverter]::GetBytes([uint16] 0), 0, $rec, $runAt, 2) # xOffset [Array]::Copy([BitConverter]::GetBytes([uint16] 16), 0, $rec, ($runAt + 2), 2) # xRun, but # the record has nowhere near 16 pixels left after this point. $stream.Write($rec, 0, $rec.Length) Set-ArtEntry $idx ($victim + $StaticBase) $appendAt 20 0 $appendAt += $rec.Length Add-Defect 'corrupt' "static/$victim" 'a 16-pixel run declared in a 20-byte record' ` 'refused by the validator' } finally { $stream.Dispose() } # 7. A land tile shorter than the 2,024 bytes LoadLand reads unconditionally. $landVictim = 0x0100 $landEntry = Get-ArtEntry $idx $landVictim if ($landEntry.Lookup -ge 0 -and $landEntry.Length -gt 0) { Set-ArtEntry $idx $landVictim $landEntry.Lookup 512 0 Add-Defect 'corrupt' "land/$landVictim" 'land record declared 512 bytes; LoadLand always reads 2,024' ` 'refused by the validator; the library reads past the buffer' } $idxDirty = $true Write-Host "" } if ($idxDirty) { [System.IO.File]::WriteAllBytes($artIdxPath, $idx) Write-Host "wrote artidx.mul" Write-Host "" } # -- Tier: verdata ---------------------------------------------------------------------------- if ($Tiers -contains 'verdata') { Write-Host "tier verdata" # Layout: int32 count, then count * 5 int32 (file, index, lookup, length, extra), then the # payloads. `lookup` is an absolute offset into this file. $entries = New-Object System.Collections.ArrayList $payloads = New-Object System.Collections.ArrayList $donorA = $donors[$donors.Count - 1] $donorB = $donors[$donors.Count - 2] $artSource = [System.IO.File]::Open($artMulPath, 'Open', 'Read', 'ReadWrite') try { $bufferA = New-Object byte[] $donorA.Entry.Length [void] $artSource.Seek($donorA.Entry.Lookup, 'Begin') [void] $artSource.Read($bufferA, 0, $bufferA.Length) } finally { $artSource.Dispose() } # The victims: ids whose art will now come from verdata.mul rather than art.mul. $legitVictim = $donors[$donors.Count - 3].Id $pastEofVictim = $donors[$donors.Count - 4].Id # A legitimate patch -- the branch working as designed. Without this the tier only proves the # failure case, and "verdata is broken" and "verdata is never reached" look identical. [void] $payloads.Add($bufferA) [void] $entries.Add([pscustomobject]@{ File = 4; Index = ($legitVictim + $StaticBase); Length = $bufferA.Length; Extra = $donorA.Entry.Extra PayloadIndex = 0; PastEof = $false }) # The failure case. FileIndex.Seek bounds-checks the mul stream and calls Verdata.Seek with no # check at all; seeking a FileStream past EOF is legal, the read returns nothing, and the # shared decode buffer still holds the previous asset. [void] $entries.Add([pscustomobject]@{ File = 4; Index = ($pastEofVictim + $StaticBase); Length = 900; Extra = 0 PayloadIndex = -1; PastEof = $true }) # An anim patch, so the tier covers the other file the verdata branch serves. anim.mul is # verdata file 6; for body < 200 the record index is body*110 + action*5 + direction. $animBody = 34 # wolf -- decodes on this client, so a patch to it is observable $animIndex = ($animBody * 110) + (0 * 5) + 1 [void] $entries.Add([pscustomobject]@{ File = 6; Index = $animIndex; Length = 700; Extra = 0 PayloadIndex = -1; PastEof = $true }) $headerSize = 4 + ($entries.Count * 20) $offset = $headerSize foreach ($entry in $entries) { if ($entry.PayloadIndex -ge 0) { $entry | Add-Member -NotePropertyName Lookup -NotePropertyValue $offset -Force $offset += $payloads[$entry.PayloadIndex].Length } } $totalSize = $offset # Past-EOF lookups are resolved last, because "past the end" is only meaningful once the end # is known. foreach ($entry in $entries) { if ($entry.PastEof) { $entry | Add-Member -NotePropertyName Lookup -NotePropertyValue ($totalSize + 8192) -Force } } $verdata = New-Object byte[] $totalSize Write-Int32LE $verdata 0 $entries.Count $at = 4 foreach ($entry in $entries) { Write-Int32LE $verdata $at $entry.File Write-Int32LE $verdata ($at + 4) $entry.Index Write-Int32LE $verdata ($at + 8) $entry.Lookup Write-Int32LE $verdata ($at + 12) $entry.Length Write-Int32LE $verdata ($at + 16) $entry.Extra $at += 20 } foreach ($entry in $entries) { if ($entry.PayloadIndex -ge 0) { $payload = $payloads[$entry.PayloadIndex] [Array]::Copy($payload, 0, $verdata, $entry.Lookup, $payload.Length) } } [System.IO.File]::WriteAllBytes((Join-Path $destFull 'verdata.mul'), $verdata) Add-Defect 'verdata' "static/$legitVictim" ` "legitimately patched to static/$($donorA.Id)'s art via verdata.mul" ` 'decodes; the picture must CHANGE, which is how we know the branch ran' Add-Defect 'verdata' "static/$pastEofVictim" ` 'verdata entry whose lookup is 8 KB past the end of verdata.mul' ` 'REFUSED BUT DECODED -- Verdata.Seek is not bounds-checked' Add-Defect 'verdata' "body/$animBody" ` "anim.mul record $animIndex patched to a verdata offset past EOF" ` 'the wolf must not silently become another creature' Write-Host (" wrote verdata.mul: {0} entries, {1:N0} bytes" -f $entries.Count, $totalSize) Write-Host "" } # -- Tier: bodyconv --------------------------------------------------------------------------- if ($Tiers -contains 'bodyconv') { Write-Host "tier bodyconv" $bodyconvPath = Join-Path $destFull 'Bodyconv.def' if (-not (Test-Path -LiteralPath $bodyconvPath)) { Write-Host " no Bodyconv.def in the copy -- skipping tier" } else { # Columns are tab-separated: original, anim2, anim3, anim4, anim5. -1 means "not in that # file". BodyConverter.Convert returns the file type of the FIRST column that is not -1, # and the extractor must take that answer and stop. $lines = @( "", "# Asset Bridge phase 0 -- deliberate defects (tools/patch_client.ps1)", "1900`t-1`t-1`t-1`t60000", "1901`t666`t-1`t-1`t-1" ) Add-Content -LiteralPath $bodyconvPath -Value ($lines -join "`r`n") -Encoding ASCII Add-Defect 'bodyconv' 'body/1900' 'mapped to anim5 index 60,000, which does not exist' ` 'reports nothing -- and must NOT fall back to another anim file' Add-Defect 'bodyconv' 'body/1901' 'mapped to anim2 index 666, where something unrelated lives' ` 'decodes a picture of the WRONG creature -- the spider case, on purpose' } Write-Host "" } # -- The source must be untouched ------------------------------------------------------------- $tampered = @() foreach ($name in $before.Keys) { $path = Join-Path $sourceFull $name $now = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash if ($now -ne $before[$name]) { $tampered += $name } } if ($tampered.Count -gt 0) { throw "THE SOURCE CLIENT WAS MODIFIED: $($tampered -join ', '). Restore it from the installer before doing anything else." } Write-Host "source client verified unchanged ($($before.Count) files hashed before and after)" # -- Manifest --------------------------------------------------------------------------------- $manifest = [pscustomobject]@{ built = (Get-Date).ToUniversalTime().ToString('u') source = $sourceFull dest = $destFull tiers = $Tiers defects = @($script:Defects) } $manifestPath = Join-Path $destFull 'patched-client.manifest.json' $manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding utf8 Write-Host "" Write-Host ("{0} deliberate defects; manifest at {1}" -f $script:Defects.Count, $manifestPath) Write-Host "" Write-Host "Point the probe at it by adding to the shard's Config/Bridge.cfg:" Write-Host "" Write-Host " AssetProbeClient=$destFull" Write-Host "" Write-Host "then, in game or from the rig driver: [assetprobe all patched"