Switch System Flaws

This page is a list of publicly known Switch flaws.

Hardware

Flaws in this category pertain to the underlying hardware that powers the Switch.

This includes components shared across Tegra based devices such as the TSEC, the Security Engine, the GPU and so on.

Summary Description Fixed with hardware model/revision Newest hardware model/revision this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
GMMU DMA attack The Switch's GPU includes a separate MMU (GMMU) that is allowed to bypass the system's IOMMU (SMMU). By accessing the GPU's MMIO region and manipulating the page table entries in the GMMU, an attacker can read/write any portion of the DRAM (except memory carveouts).

[5.0.0+] Works around this hardware flaw by using memory pool partitioning. You can no longer escalate into sysmodules with GPU DMA because all their memory is allocated using heap that's carved out.

HAC-001-01 (Mariko/Tegra214/Tegra210b01): Fixes this by adding a new register which restricts what memory untranslated DMA requests may access. Untranslated GPU DMA may now only access the GPU carveout (physmem 0x80002000-0x80006000), which the GPU already has legitimate and exclusive access to.

HAC-001-01 (Mariko/Tegra214/Tegra210b01) HAC-001 (Tegra210) Summer 2017 December 28, 2017 hexkyz, SciresM and qlutoo
Weak Security Engine context validation The Tegra X1 supports a "deep sleep" feature, where everything but DRAM and the PMC registers lose their content (and the SoC loses power). Upon awaking, the bootrom re-executes, restoring system state. Among these stored states is the Security Engine's saved state, which uses AES-128-CBC with a random key and all-zeroes IV. However, the bootrom doesn't perform a MAC on this data, and only validates the last block. This allows one to control most of security engine's state upon wakeup, if one has a way to modify the encrypted state buffer.

With a way to modify the encrypted state buffer, one can thus dump keys from "write-only" keyslots, etc.

This also bypasses the SBK protection of the bootROM: indeed, at warmboot, bootROM will always clear keyslot 0xE to prevent malicious code from saving the SBK. Moving the SBK to another keyslot in the saved context renders this protection moot.

HAC-001-01 (Mariko/Tegra214/Tegra210b01): Fixes this by streamlining the context save process; security engine contexts are now saved to protected memory which the CPU cannot access or modify.

HAC-001-01 (Mariko/Tegra214/Tegra210b01) HAC-001 (Tegra210) December 2017 January 20, 2018 SciresM and motezazer
Security Engine keyslots vulnerable to partial overwrite attack

The Tegra X1 security engine supports writing keyslot data to the engine with syntax as follows:

SECURITY_ENGINE->AES_KEYTABLE_ADDR = (keyslot << 4) | (dword_index_in_keyslot);

SECURITY_ENGINE->AES_KEYTABLE_DATA = readle32(key, dword_index_in_keyslot * 4);

However, the Security Engine flushes writes to the internal key tables immediately when AES_KEYTABLE_DATA is written -- this allows one to overwrite a single dword of a key at a time, and thus brute force the contents of keyslots in time (2^32 * 8) = 2^35 instead of 2^256.

None HAC-001 (Tegra210) Theorized Summer 2017 due to suggestive syntax, confirmed April 9, 2018 April 9, 2018 SciresM, almost surely others (independently).
CVE-2018-6242 (leveraged by the ShofEL2 and Fusée Gelée exploits) The USB software stack provided inside the boot instruction rom (IROM/bootROM) contains a copy operation whose length can be controlled by an attacker. By carefully constructing a USB control request, an attacker can leverage this vulnerability to copy the contents of an attacker-controlled buffer over the active execution stack, gaining control of the Boot and Power Management processor (BPMP) before any lock-outs or privilege reductions occur. This execution can then be used to exfiltrate secrets and to load arbitrary code onto the main CPU Complex (CCPLEX) "application processors" at the highest possible level of privilege (typically as the TrustZone Secure Monitor at PL3/EL3). HAC-001-01 (Mariko/Tegra214/Tegra210b01) (also fixed independently on Tegra186). HAC-001 (Tegra210) January 2018 April 23, 2018 shuffle2 and fail0verflow (originally),
ktemkin and ReSwitched Team (independently),
naehrwert (independently),
hexkyz (independently),
st4rk with Shiny Quagsire and Dazzozo (independently),
and many others (independently).
Poor validation of bootrom SDRAM configuration parameters leads to arbitrary writes in bootrom

The Tegra X1 bootrom supports saving SDRAM parameters to scratch registers, and using the saved configuration to enable DRAM during warmboot.

The code that parses these parameters does if (params->EmcBctSpareN) *params->EmcBctSpareN = params->EmcBctSpareNPlusOne for most N, without validating either the address or value written to it. There are other arbitrary writes in this code, as well (e.g. BootromPatch parameters intended for patching MISC registers do not check a relative offset to 0x7000000, etc).

This allows a user with access to the PMC registers (via pre-sleep bpmp execution, or otherwise) to gain arbitrary bootrom code execution.

HAC-001-01 (Mariko/Tegra214/Tegra210b01): Fixes this by validating that the spare writes/bootrom patch before performing them.

HAC-001-01 (Mariko/Tegra214/Tegra210b01) HAC-001 (Tegra210) 2017 December 16, 2018 Everyone (independently).
TSEC ROM does not clear crypto registers after signature verification

TSEC supports executing signed-microcode at a greater privilege level than normal payloads.

When jumping to signed microcode, the caller is expected to load hardware crypto register $c6 = <signature>, $c7 = <seed (zero for all officially-signed microcode)>.

TSEC ROM then calculates the expected signature and compares it to the user-supplied one in $c6. On match, the secure payload is executed, and on failure an exception is raised.

However, TSEC ROM fails to clear the crypto registers used to calculate the expected signature in either of the success/failure cases.

Thus, with some way of obtaining the contents of crypto registers (e.g. ROP under some secure payload), an attacker can dump intermediary values from signature calculation.

With enough data/trial/error, this is enough to reconstruct the signature algorithm:

  • mac = <davies meyer hash of (page || address of page) for each 0x100 page in the payload>
  • key = AES-ENCRYPT(hardware csecret 0x1, seed)
  • signature = AES-ENCRYPT(key, mac)
None HAC-001 (Tegra210) Late 2018/Early 2019 August 2020 qlutoo/hexkyz/shuffle2, SciresM/motezazer (independently).
TSEC signature validation design flaw leads to fake-signing

As mentioned above, when jumping to signed microcode the caller is expected to load hardware crypto register $c6 = <signature>, $c7 = <seed (zero for all officially-signed microcode)>.

However, TSEC ROM performs no validation on the input seed used to generate the signing key.

This leads to the following attack:

  • Attacker gains rop under any secure microcode payload with signature = S.
  • Attacker uses the "csigenc" instruction to obtain K = AES-ENCRYPT(hardware csecret 0x1, S).
  • Attacker jumps to their own microcode with $c6 = <signature calculated on pc using K>, $c7 = S
  • TSEC ROM calculates key = AES-ENCRYPT(hardware csecret 0x1, S) = K, and the signature check passes.
  • Attackers microcode is executed in secure mode as though it were signed by NVidia.

Thus an attacker who has exploited *any* secure payload may use this to obtain a "fake signature key", which can be used to sign and execute arbitrary microcode in secure mode.

Note: this does not break the TSEC cryptosystem, as the csigenc mechanism relies on the signature of the executing microcode, and fakesigning produces different signatures from NVidia that cannot be controlled.

None HAC-001 (Tegra210) Late 2018/Early 2019 August 2020 qlutoo/hexkyz/shuffle2, SciresM/motezazer (independently).
ROP under TSEC secure bootrom via DMA engine stack overwrite (--xploit) TSEC DMA engine does not stop when entering TSEC secure bootrom. By pointing TSEC DMA to current stack before secure bootrom entry, stack can be controlled.

One can then use blind ROP against the TSEC secure bootrom (which is execute only, and cannot be dumped).

With sufficient effort, an attacker can construct a ROP chain that leads to csigcmp being executed with fully controlled arguments.

This allows for arbitrary heavy secure mode code execution with the current signature set to an arbitrary value.

This completely breaks the TSEC cryptosystem, by allowing one to obtain the result of csigenc with signature = <any desired value>.

This has many uses/results, notably including dumping the "true" signature key (set signature = zeroes, perform csigenc using csecret 0x1).

None TSEC for all Tegra devices Late 2018 January 2021 hexkyz/SciresM, Vale/Thog (independently), Tatsuko (independently), possibly others (independently).
Boot straps are not relatched on watchdog resets (strapwn) On boot, the BOOTSELECT, RCM and RAM_CODE straps are latched from external GPIO to determine which boot medium to use and verify from in bootrom. However, APB_MISC_PP_STRAPPING_OPT_A can be overwritten with arbitrary values following bootrom. Write access to PP_STRAPPING_OPT_A would otherwise be mundane, however these straps are not relatched during a watchdog reset (despite being latched during other software resets), allowing for arbitrary straps to be selected and executed in bootrom.

This allows setting NVPROD_UART on some hardware configurations where it would normally be unavailable (ie on Jetson Nano boards), but is otherwise mostly useless and/or useful for testing unintended boot options (such as USB Mass Storage boot) without having to move boot strap resistors.

Unknown HAC-001 (Tegra210) May 2020 April 30, 2021 Shiny Quagsire

Firmware

Flaws in this category pertain to the firmware running on hardware devices, such as wifi/bluetooth, etc. Firmware is generally uploaded by sysmodules.

Summary Description Successful exploitation result Fixed in system version Last system version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
Broadpwn (CVE-2017-9417) See here and here. Code execution on the wifi controller (untested on Switch). 4.0.0 4.0.0 Switch: July 2022 Switch: July 30, 2022 Switch: yellows8

Software

Bootloader

Flaws in this category pertain to any bootloader component such as the package1ldr, the NX bootloader or the warmboot binary.

Summary Description Successful exploitation result Fixed in system version Last system version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
Null-dereference in panic() The Switch's stage 1 bootloader, on panic(), clears the stack and then attempts to clear the Security Engine. However, it does so by dereferencing a pointer to the SE in .bss (initially NULL), and this pointer doesn't get initialized until partway into the bootloader's main() after several functions that might panic() are called. Thus, a panic() caused prior to SE initialization would result in the SE pointer still being NULL when dereferenced.

The BPMP doesn't have an active MPU and the bus won't data abort on an invalid address, so no exception will be entered: it'll end up overwriting some exception vectors with NULL before halting.

In 3.0.0, this was fixed by moving the security engine initialization earlier in main(), before the first function that could potentially panic().

Some exception vectors overwritten with NULL, before SBK/other keyslots are cleared. Probably useless for anything more interesting. 3.0.0 3.0.0 Early July, 2017 July 30, 2017 Everyone who diff'd 2.3.0 and 3.0.0 Package1
FUSE_DIS_PGM not written by package1 The switch's hardware fuse driver contains a write-once bit in a register called "FUSE_DIS_PGM", which disables burning fuses until the next reboot. While Nintendo's bootloader code for waking up from sleep writes this on all firmware, the actual package1 initial bootloader forgets to write to it on cold reboot.

This isn't too big of a problem because another fuse is burnt on retail devices (production mode), which prevents burning *all* fuses other than ODM_RESERVED ones in hardware.

This was fixed in 3.0.0 by writing to the register on cold boot (although the write happens in TZ instead of package1 where it should take place, possibly to obfuscate the fact that they made this mistake).

Burning arbitrary ODM reserved fuses with TZ code execution, which should never be possible for non-bootloader code.

Warning: one could irreparably brick one's console by playing with this.

3.0.0 3.0.0 Late summer/early fall 2017 December 31, 2017 SciresM, motezazer
maconstack (TSEC firmware leaves MAC on the stack) Package1ldr loads a firmware blob into TSEC early on boot. This piece of code runs on the TSEC in Authenticated Mode and has the sole purpose of generating the per-console TSEC key (see Cryptosystem).

As a way to mitigate attacks, the TSEC firmware blob is split into 3 stages: Boot which is unencrypted and unsigned, KeygenLdr which is unencrypted but signed and Keygen which is encrypted and signed. Boot loads a static pre-generated signature into the Falcon's CPU crypto registers, loads KeygenLdr into the Falcon's CODE region and jumps to it. Execution will proceed into KeygenLdr in Heavy Secure Mode if, and only if, the loaded signature matches the one Falcon calculates internally for KeygenLdr.

Among various things, KeygenLdr will attempt to do a "backwards" security check by calculating a CMAC over Boot and comparing it with a known hash stored in the TSEC firmware's key data (a small buffer stored after Boot's code). If the hashes don't match, execution aborts.

KeygenLdr stores the calculated Boot's CMAC in the stack, but forgets to clear it. Since the stack is located in Falcon's DATA region, loading the TSEC firmware blob and dumping the DATA region afterwards (via MMIO) will reveal the calculated hash. This allows using KeygenLdr as an oracle to generate a valid CMAC for arbitrary Boot code. Replacing the CMAC in the TSEC firmware's key data region results in KeygenLdr accepting any Boot code, thus rendering this security measure useless.

Additionally, since signed Falcon code can't be revoked without an hardware revision, an attacker can always reuse the flawed KeygenLdr code even if a fix is issued.

Running TSEC firmware's KeygenLdr in a user controlled environment. None 5.0.2 January 2018 April 29, 2018 hexkyz, Reisyukaku (independently), probably others (independently).
pk1ldrhax Package1ldr decrypts and verifies the keyblob inside of the current BCT in order to get the package1 key, and then uses the package1 key to decrypt package1. It then validates package1 before jumping to it by checking the PK11 magic number, and that the section sizes sum to the expected size (and are individually less than the expected size).

However, package1ldr does not actually validate the package1 key against a fixed vector (much like kernel9loader forgot to do so on the 3ds). This would normally not matter, as keyblobs are validated -- however, with bootrom code execution one can dump SBK and forge keyblobs, and thus control the package1 key.

Thus (in theory, but not in practice due to the size of the brute force required) one can replace the package1 key with garbage, causing package1 to decrypt into garbage, and hope that this garbage passes validation checks and that package1ldr jumping into the garbage will do something useful.

This was fixed incidentally in 6.2.0, as pk1ldr does not use keyblob data to decrypt package1 any more.

With a large enough brute force: arbitrary package1 code execution from coldboot.

However, a usable brute force is on the order of >= ~2^80, so this is almost certainly not actually usable in any meaningful context.

6.2.0 6.2.0 Early 2017 (as soon as plaintext package1ldr was first dumped) November 20, 2018 Everyone
Stack smash in TSEC firmware's KeygenLdr Given that we can control the key data (which is not authenticated) and the Boot blob (see "maconstack"), as well as the fact Non-secure and Heavy Secure code share the same stack, we can use this to attack KeygenLdr. KeygenLdr uses memcpy to copy over a payload to DMEM to verify it, which can be abused to smash the stack (in DMEM) and write over the return address of said function. ROP under KeygenLdr in Heavy Secure mode. None 8.0.1 Early 2018 May 21, 2019 Everyone (independently).

TrustZone

Flaws in this category pertain exclusively to the Secure Monitor.

Summary Description Successful exploitation result Fixed in system version Last system version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
Non-atomic mutexes When an SMC is called, TrustZone sets a global variable to mark that an SMC is in progress, so that two SMCs using shared resources (like the security engine) do not trample on one another. On 1.0.0, this global variable was written using non-atomic writes, and thus a race condition is possible.

However, the SMC handler enforces that all SMCs must be called from core #3, unless the top-level handler ID is 1 (SMCs internal to the kernel). Thus, the only SMCs that can be run side-by-side are [any userland smc] and smcGetRandomBytesForKernel, and this turns out to not really be abusable.

Mostly useless. Maybe some oob-write into unused (and thus useless) memory if running smcGetRandomBytesForKernel and smcGetRandomBytesForUser at the same time. 2.0.0 2.0.0 December 2017 (Probably earlier by others) January 18, 2018 SciresM, probably others (independently).
jamais vu (non-secure world access to PMC MMIO and pre-deep sleep firmware) On 1.0.0, one could map in the PMC registers in userland. In addition, am ran a little-kernel based firmware on the BPMP at runtime. With code execution under am, one could modify the BPMP's little-kernel firmware to hook deep sleep entry, and modify TrustZone/Security engine state.

This was fixed in 2.0.0 by making the PMC secure-world only, blacklisting the BPMP's exception vectors from being mapped, and thoroughly checking for malicious behavior on deep sleep entry.

Arbitrary TrustZone code execution. 2.0.0 2.0.0 December, 2017 January 20, 2018 SciresM and motezazer
Missed BPMP Exception Vector Writes Starting in 2.0.0, the BPMP is asleep at runtime, and is turned on by TrustZone during smcCpuSuspend in order to initiate the deep sleep process. When it does so, it is held in RESET, and TrustZone attempts to write to the BPMP exception vectors at 0x6000F200 to register EVP_RESET = lp0_entry_fw_crt0, and all other EVPs to a function that simply reboots. However, while they successfully write EVP_RESET, they miss all the other vectors, accidentally writing to the 0x6000F004-0x6000F020 region instead of the 0x6000F204-0x6000F220 region they want to write to. This results in all the exception vectors for the BPMP other than RESET being "undefined" (attacker controlled).

With some way of causing an exception vector to be taken at the right time, this would give pre-sleep code execution (and thus arbitrary TrustZone code execution, via the security engine flaw). However, none of the abort vectors are really triggerable, and interrupts are disabled for the BPMP when it is taken out of reset. Thus, this is useless in practice.

This was fixed in 4.0.0 by writing to the correct registers.

Theoretically: Arbitrary TrustZone code execution. In practice: Useless. 4.0.0 4.0.0 January, 2018 February 23, 2018 SciresM and motezazer, naehrwert, hexkyz, probably others (independently).
TSEC has access to the secure kernel carveout TrustZone is responsible for managing security carveouts to prevent DMA controllers from accessing the carveout which contains the kernel, sysmodules, and other critical operating system data.

Until 8.0.0, the list of devices that could access the carveout included the TSEC. However, the TSEC can bypass the SMMU when in authenticated mode by writing to a certain register. Thus, pwning nvservices would allow one to take over the TSEC, and use it to write to normally protected mmio/memory.

In 8.0.0, this was fixed by removing TSEC access, and adding TSECB access (TSECB cannot bypass the SMMU).

With access to the TSEC mmio (nvservices ROP) and code execution in TSEC Heavy Secure mode, kernel code execution, probably. 8.0.0 8.0.0 2017 (when TrustZone code plaintext was first obtained). April 15, 2019 Everyone
deja vu (insufficient system state validation on suspend leads to pre-sleep BPMP code execution) Jamais Vu was fixed in 2.0.0 by making the PMC secure-world only, blacklisting the BPMP's exception vectors from being mapped, and thoroughly checking for malicious behavior on deep sleep entry, since gaining pre-sleep code execution on the BPMP compromises the system.

However, the state validation performed by Nintendo's Secure Monitor was insufficient to prevent pre-sleep execution from being obtained.

Prior to 6.0.0, one could use a DMA controller that had access to IRAM and was not held in reset (there were multiple) to race TrustZone's writes to the BPMP firmware in IRAM, and thus overwrite Nintendo's firmware with an attacker's to gain pre-sleep code execution.

6.0.0 addressed this by performing TrustZone state MAC writes and locking PMC scratch *before* turning on the BPMP, fixing the original Jamais Vu exploit entirely. In addition, the BPMP firmware in TrustZone's .rodata is now memcmp'd to the actual data after it is written to IRAM. This mitigates race attacks that modify the firmware.

However, Nintendo both forgot to validate the BPMP exception vectors after writing them, and forgot to hold in reset a DMA controller that can write to the BPMP's exception vectors.

AHB-DMA is not blacklisted by kernel mapping whitelist (Nintendo probably forgot it, because the TX1 TRM does not really document that it's present, although the MMIO works as documented in older (Tegra 3 and before) TRMs).

Thus, with kernel code execution (or some other way of accessing AHB-DMA, e.g. nspwn on <= 4.1.0, TSEC hax, or other arbitrary mmio access flaws), one can DMA to the BPMP's exception vectors as they are written, causing TrustZone to start the BPMP executing an attacker's firmware at a different location than TrustZone intends/validates.

This was fixed in 8.0.0 by blocking AHB-DMA arbitration and verifying it is held in reset during suspend, and thus there are no more devices that can write to the relevant MMIO at the right time.

Arbitrary TrustZone/BootROM code execution, by using either the original Jamais Vu flaw (prior to 6.0.0 or a warmboot bootrom exploit (any firmware where pre-sleep execution can be gained). 8.0.0 8.0.0 December 2017 April 15, 2019 SciresM, motezazer and ktemkin, naehrwert (independently), almost certainly others (independently)
TrustZone allows using imported RSA exponents with arbitrary modulus TrustZone supports "importing" RSA private exponents for use by userland -- these are stored encrypted with TrustZone only keydata in NAND, and decrypted only to TZRAM. This prevents a console that has compromised userland from learning the private exponents of these keys and doing calculations with them offline. In practice, this is used for FS (gamecard communications), ES (drm), and SSL (console client cert communications).

However, the actual SMC API only imports the RSA exponent, and not the modulus, which is passed separately by userland in each call. There is no validation done on the modulus passed in -- this means that userland can pass in any message and modulus it chooses, and obtain the result of (message ^ private exponent) % modulus back from the secure monitor.

By choosing a prime number modulus P such that P has "smooth" order (totient(P) == P-1 is divisible only by "small" primes), one can efficiently use the Pohlig-Hellman algorithm to calculate the discrete logarithm of such a result directly, and thus obtain the private exponent.

This is mostly useless in practice, given the general availability of other exploits to obtain these decrypted exponents.

This was fixed in 10.0.0 by importing the modulus in addition to the exponent for the ES device key and ES client cert key. For backwards compatibility reasons the SSL key and Lotus key still only import the exponent.

StorageExpMod also now validates that the exponentiation of "DDDDD..." about the provided modulus by the imported exponent and then the fixed public exponent returns "DDDDD...", and returns invalid argument if validation fails.

With userland privileges sufficient to use an imported RSA key: obtaining that RSA key's private exponent. 10.0.0 10.0.0 August 14, 2019 August 14, 2019 SciresM

Kernel

Flaws in this category pertain exclusively to the HorizonOS Kernel.

Summary Description Successful exploitation result Fixed in system version Last system version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
Syscall Infoleaks Many syscalls leaked kernel pointers on sad paths (for example svcSetHeapSize and svcQueryMemory), until they landed a bunch of fixes in 2.0.0. Nothing really. 2.0.0 2.0.0 2017 Everyone
svcWaitSynchronization/svcReplyAndReceive bad cleanup on error If there is a page fault when fetching handles from the userspace array, it cleans up by dereferencing all objects despite having only loaded first N. Allows the attacker to make arbitrary decrefs on any kernel synchronization object, and thus can be used to get UAF. Haven't actually been tried on real HW though, but should work (tm). Kernel code execution 2.0.0 2.0.0 April 24, 2017 qlutoo
Bad irq_id check in CreateInterruptEvent CreateInterruptEvent syscall is designed to work only for irq_id >= 32. All irq_ids < 32 are "per-core" and reserved for kernel use (watchdog/scheduling/core communications).

On 1.0.0 you could supply irq_id < 32 and it would write outside the SharedIrqs table.

You can register irq's in the Core3Irqs table, and thus register per-core irqs for core3, that are normally reserved for kernel. Useless. 2.0.0 2.0.0 October 2017 October 17, 2017 qlutoo
Kernel .text mapped executable in usermode Prior to 3.0.2 the kernel .text was mapped in usermode as executable. This can be used for usermode ROP for bypassing ASLR, but SVCs/IPC are not usable by running kernel .text in usermode. Executing kernel .text in usermode 3.0.2 3.0.2 December 28, 2017 (34c3) qlutoo
Memory Controller not properly secured The Switch OS originally had the memory controller not set to be accessible only by the secure-world, which was problematic because insecure access can compromise the kernel.

This was fixed partially in 2.0.0 by blacklisting the memory controller from being mapped by user-processes, and was fixed entirely in 4.0.0 by making the memory controller TZ-only and making all kernel accesses go through smcReadWriteRegister.

With some way to access the memory controller MMIO, arbitrary kernel code execution. 4.0.0 4.0.0 January 2018 January 2018 SciresM, yellows8
Potential svcWaitForAddress thread use-after-free Between 4.0.0, where svcWaitForAddress was introduced, and 7.0.0, there was a second intrusive rbtree node in KThread for the WaitForAddress tree (the key being (address, priority), sorted lexicographically). Unlike the WaitProcessWideKeyAtomic tree, the kernel forgot to reinsert the WaitForAddress node when the thread's priority changed (priority inheritance and/or SetPriority), breaking the rbtree invariants; and since the kernel walks through the entire tree to remove intrusive nodes, you could cause threads to stay in the tree even after their deletion.

7.0.0 fixed the issue by using the same intrusive node for both trees. The thread/node knows which tree it is in, and the latter is correctly updated when thread priority changes.

It unluckily didn't look exploitable 7.0.0 7.0.0 July 2018 February 2019 TuxSH
Kernel RWX identity mapping never unmapped During init, the kernel binary is identity-mapped as RWX at 0x80060000; this is necessary to facilitate the transitionary period while the MMU is being enabled but mappings for e.g. KASLR are not yet determined, and also to enable smooth MMU enable transition during wake-from-sleep.

However, the identity mapping was never unmapped, and thus the whole kernel code bin remained permanently mapped as RWX for all kernel threads (any thread which does not have an owner process and thus uses the KSupervisorPageTable TTBR0).

Thus, any theoretical exploit which would give kernel memory corruption or ROP under a kernel thread would allow making use of this mapping to modify kernel text + bypass KASLR.

This was fixed in 16.0.0 by unmapping the identity-mapping during init, and re identity-mapping only the very first page of kernel .text as R-X (for use by wake-from-sleep), which fixes the shellcode problem and mostly fixes the ROP problem, since this page mostly lacks interesting gadgets.

In theory, with another exploitable kernel memory corruption (or ROP under kernel thread) bug: bypassing KASLR + modifying kernel .text.

However, no such bugs are known.

16.0.0 16.0.0 Summer 2018 February 2023 Everyone

BootImagePackage System Modules

Flaws in this category pertain to any of the built-in system modules.

Summary Description Successful exploitation result Fixed in system version Last system version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
Service access control bypass (sm:h, smhax, probably other names) Prior to 3.0.1, the service manager (sm) built-in system module treats a user as though it has full permissions if the user creates a new "sm:" port session but bypasses initialization. This is due to the other sm commands skipping the service ACL check for Pids <= 7 (i.e. all kernel bundled modules) and that skipping the initialization command leaves the Pid field uninitialized.

In 3.0.1, sm returns error code 0x415 if Initialize has not been called yet.

Acquiring, registering, and unregistering arbitrary services 3.0.1 3.0.1 May 2017 August 17, 2017 Everyone
Overly permissive SPL service The concept behind the switch's Secure Monitor is that all cryptographic keydata is located in userspace, but stored as "access keys" encrypted with "keks" that never leave TrustZone. The spl ("security processor liaison"?) service serves as an interface between the rest of the system and the secure monitor. Prior to 4.0.0, spl exposed only a single service "spl:", which provided all TrustZone wrapper functions to all sysmodules with access to it. Thus anyone with access to the spl: service (via smhax or by pwning a sysmodule with access) could do crypto with any access keys they knew.

This was fixed in 4.0.0 by splitting spl: into spl:, spl:mig, spl:ssl, spl:es, and spl:fs.

Arbitrary spl: crypto with any access keys one knows. For example, one could use the SSL module's access keys to decrypt their console's SSL certificate private key without having to pwn the SSL sysmodule. 4.0.0 4.0.0 Summer 2017 (after smhax was discovered). December 23, 2017 Everyone
Single session services not really single session Several "critical" services (like fsp-ldr, fsp-pr, sm:m, etc) are meant to only ever hold a single session with a specific sysmodule. However, when a sysmodule dies, all its service session handles are released -- and thus killing the holder of a single session handle would allow one (via sm:hax etc) to get access to that service.

This was fixed in 4.0.0 by adding a semaphore to these critical single-session services, so that even if one gets access to them an error code will be returned when attempting to use any of their commands.

With some way to access these services and kill their session holders (like expLDR): dumping sysmodule code, arbitrary service access, elevated filesystem permissions, etc. 4.0.0 4.0.0 May/June 2017 (basically immediately after smhax was discovered) December 30, 2017 Everyone
nspwn fsp-ldr command 0 "MountCode" takes in a Content Path (retrieved from NCM by Loader), and returns an IFileSystem for the resulting ExeFS. These content paths, are normally NCAs, but MountCode also supports a number of other formats, including ".nsp" -- which is just a PFS0.

When a path ending in ".nsp" is parsed by MountCode, the PFS0 is treated as a raw ExeFS. Because there is no NCA header, the ACID signatures are not validated -- and because there are no other signatures in a PFS0, this results in no signature checking happening at all.

The actual .nsp handling is eventually done by {content mounting function} called by MountCode and other FS commands.

Thus, by placing an ExeFS (NSOs + "main.npdm") and setting one's desired title ID to "@Sdcard:/some_title.nsp" or "@User:/some_title.nsp" etc one can launch arbitrary unsigned code, with arbitrary unsigned NPDMs.

This appears to have been fixed by only allowing .nsp when the input fstype==7 for the internal content-mounting function, returning 0x2EE202 otherwise.

With access to "lr": Arbitrary code execution with full system privileges. 5.0.0 5.0.0 Late 2017 April 23, 2018 Everyone
Single null-byte stack overflow in Loader ContentPath parsing Previously, loader content path parsing looked like this, where path_from_lr was up to 0x300 bytes and not necessarily null-terminated:
 char nca_path[0x300] = {0};
 strcat(nca_path, path_from_lr);
 for (int i = 0; nca_path[i]; i++) {
     if (nca_path[i] == '\\') { nca_path[i] = '/'); }
 }

Thus, a content path of the maximum length (0x300 bytes) would result in strcat writing a NULL terminator past the end of the nca_path buffer.

This was fixed in 6.0.0, the new code looks like this:

 char nca_path[0x300];
 strncpy(nca_path, path_from_lr, sizeof(nca_path));
 for (int i = 0; i  < sizeof(nca_path) && nca_path[i]; i++) {
     if (nca_path[i] == '\\') { nca_path[i] = '/'); }
 }


With access to "lr": single null-byte stack overflow in Loader. Maybe (but probably not) loader code execution. 6.0.0 6.0.0 September 2, 2018 September 19, 2018 SciresM
System modules vulnerable to selective downgrade attacks Horizon has no mechanism for specifying the specific title version to Loader on process creation.

Observing this, one can note that after a system update one could install a downgraded version of a specific system module (e.g. nvservices) while leaving the rest of the OS at the same version.

Unless there was some breaking API change, this allows one to make a console vulnerable once more to an exploit in a sysmodule by downgrading it and nothing else.

This was fixed in 8.1.0 by incrementing a version field in NPDM, and checking it against a hardcoded list for certain titles in Loader's process creation func.

With access to content installation commands (or a vulnerable lower version to selectively install newer titles), reintroducing bugs in vulnerable system modules on newer firmware versions. 8.1.0 8.1.0 When FIRM was first dumped in 2017. June 17, 2019 Everyone
Broken RNG for Loader ASLR The RNG used for generating the ASLR slide is only seeded with 32bits, with the data from svcGetInfo. Hence, one could bruteforce the seed if one has infoleaks from any programs. This can be successfully bruteforced with at least 2 sample codebin addrs from different programs (with only 1 sample a lot of invalid seeds are found), however in some cases more than 1 seed might be found.

With [15.0.0+] Loader now uses csrng_GenerateRandomBytes for determining the ASLR slide.

See also loader-aslr-solver.

Breaking ASLR for all non-KIP processes, allowing predicting the main-codebin base addr for all non-KIP processes until the next reboot. 15.0.0 15.0.0 January 30, 2022 (presumably found much earlier?) October 11, 2022 Everyone

System Modules

Flaws in this category pertain to any non-built-in system module.

Summary Description Successful exploitation result Fixed in system version Last system version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
OOB Read in NS system module (pl:utoohax, pl:utonium, maybe other names) Prior to 3.0.0, pl:u (Shared Font services implemented in the NS sysmodule) service commands 1,2,3 took in a signed 32-bit index and returned that index of an array but did not check that index at all. This allowed for an arbitrary read within a 34-bit range (33-bit signed) from NS .bss. In 3.0.0, sending out of range indexes causes error code 0x60A to be returned. Dumping full NS .text, .rodata and .data, infoleak, etc 3.0.0 3.0.0 April 2017 June 19, 2017 qlutoo, ReSwitched Team (independently)
Unchecked domain ID in common IPC code Prior to 2.0.0, object IDs in domain messages are not bounds checked. This out-of-bounds read could be exploited to brute-force ASLR and get PC control in some services that support domain messages. 2.0.0 2.0.0 July 2017 July 20, 2017‎ hthh
Out-of-bounds array read for BCAT_Content_Container secret-data index The BCAT_Content_Container secret-data index is not validated at all. This is handled before the RSA-signature(?) is ever used. Since the field is an u8, a total of 0x800-bytes relative to the array start can be accessed.

This is not useful since the string loaded from this array is only involved with key-generation.

Unknown 2.0.0 August 4, 2017 August 6, 2017 Shiny Quagsire, yellows8 (independently)
expLDR (sysmodule handle table exhaustion) Most sysmodules share common template code to handle IPC control messages. The command DuplicateSession (type 5 command 2)'s template code will abort() if it fails to duplicate a session's handle for the requester. Because many sysmodules have limited handle table size (smaller than the browser/other entrypoints), repeatedly requesting to duplicate one's session will cause the sysmodule to run out of handle table space and abort, causing the service to release all its handles cleanly. Sysmodule crashes. Most usefully, crashing ldr allows access to fsp-ldr and crashing pm allows access to fsp-pr. Useless after 4.0.0, which mitigated a number of single-session service access issues. Unfixed 4.1.0 June 24, 2017 March 8, 2018 daeken
Transfer Memory leak in nvservices system module The nvservices sysmodule does not clear most of its transfer memory prior to release. The calling process can read key bits of memory, including breaking ASLR (by revealing the image base) and exposing the address of other transfer memory to set up attacks. More details here: transfermeme (nvservices info leak) by daeken 6.0.0 6.0.0 June 2017 October 16, 2018 qlutoo and hexkyz,

daeken (independently)

OOB write in audio system module Prior to 2.0.0, the AppendAudioOutBuffer and AppendAudioInBuffer IPC commands would blindly increment the appended buffers' count while using said count value as an index to where the user data should be copied into. This resulted in an 0x28 bytes, user controlled, out-of-bounds memory write into the audio sysmodule's memory space.

Combined with the GetReleasedAudioOutBuffer or GetReleasedAudioInBuffer commands, this could also be used as an 8 byte infoleak.

In 2.0.0, the commands now return error code 0x1099 if the number of unreleased buffers exceeds 0x1F.

Code execution under audio sysmodule 2.0.0 2.0.0 November 2, 2018 hexkyz, probably others (independently).
Infoleak in nvservices system module The nvservices ioctl NVMAP_IOC_ALLOC takes an optional argument "addr" which allows the calling process to pass a pointer to user allocated memory for backing a nvmap object. If "addr" is left as 0, nvservices uses the transfer memory region (donated by the user during initialization) instead, when allocating memory for the nvmap object.

By design, freeing the nvmap object by calling the ioctl NVMAP_IOC_FREE returns, in its "refcount" argument, the user address previously supplied if the reference count reaches 0. However, prior to 6.2.0, the case where the transfer memory region is used to allocate the nvmap object was not taken into account, thus resulting in NVMAP_IOC_FREE leaking back an address from within the transfer memory region mapped in nvservices' memory space.

In 6.2.0, NVMAP_IOC_FREE no longer returns the address when the transfer memory region is used instead of user supplied memory.

Combined with other vulnerabilities: Defeating ASLR in nvservices sysmodule. 6.2.0 6.2.0 April 2017 November 24, 2018 Everyone
nvhax (memory corruption in nvservices system module) Prior to 6.2.0, the nvservices ioctl NVGPU_GPU_IOCTL_WAIT_FOR_PAUSE would take a single "pwarpstate" argument which would be interpreted by nvservices as a memory pointer for writing 2 "warpstate" structs (one for each Streaming Multiprocessor).

This resulted in nvservices attempting to blindly memcpy into this user supplied address and trigger a crash. However, if paired with an infoleak, this could be used to arbitrarily write 0x30 bytes anywhere in nvservices' memory space. Additionally, the "warpstate" struct itself was never initialized, which means nvservices would leak the 0x30 bytes from the stack. By invoking other ioctls it was also possible to partially control the stack contents and achieve a usable arbitrary memory write primitive.

In 6.2.0, NVGPU_GPU_IOCTL_WAIT_FOR_PAUSE now takes 2 inline "warpstate" structs instead of a "pwarpstate" pointer, thus effectively avoiding the bad memcpy.

Code execution under nvservices sysmodule 6.2.0 6.2.0 April 5, 2017 November 24, 2018 hexkyz
AM IStorage infoleak Originally the buffer allocated by CreateStorage using the specified input size was not cleared. With [8.0.0+] this was fixed by adding a memset() for the buffer after successful allocation.

Hence, IStorage->IStorageAccessor->Read will return uninitialized memory when the Write cmd was not previously used with the specified region.

Infoleak from the main AM heap, allowing defeating ASLR by reading addresses from previously allocated objects. 8.0.0 8.1.0 December 2018 August 9, 2019 yellows8
hid:sys ButtonConfig s32 array-index not validated The input s32 array-index for hid:sys ButtonConfig cmds 1255-1270 was originally not validated. Using a negative or >=5 index results in accessing out-of-bounds data, with an array stored on stack.

[10.1.0-10.2.0] Each of these cmds will now Abort if the s32 is negative or >=5. [11.0.0+] Now an unsigned compare is used, with 0 or an error being immediately returned when the value is invalid.

hid infoleak, out-of-bounds mem-write anywhere in hid address-space relative to the stack array (with constraints on the data). 10.1.0 11.0.1 April 18, 2020 July 14, 2020 yellows8
Bluetooth sdp_server.cc process_service_search() continuation request p_req validation With [5.0.0+], the following was added to the if-block prior to loading cont_offset from p_req: (p_req + sizeof(cont_offset) > p_req_end) (which verifies that cont_offset is within message bounds). Bluetooth-sysmodule out-of-bounds read from heap, probably not useful since the read value must match a state field, etc. 5.0.0 11.0.0 Switch: December 2020 Switch: December 25, 2020 Switch: yellows8
Bluetooth A-63146698 A-63146698 / CVE-2017-0785. See also here. Bluetooth-sysmodule stack infoleak, which allows defeating ASLR (note: not tested on hw). 5.0.0 11.0.0 Switch: December 2020 Switch: December 25, 2020 Switch: yellows8
bluetooth GetAdapterProperty/SetAdapterProperty unchecked memcpy size GetAdapterProperty copies data from stack to the output buffer using the buffer size, without checking the size (when not handling the Name type). SetAdapterProperty copies data to stack from the input buffer using the buffer size, without checking the size.

This requires access to the btdrv service, only hid and btm have access.

This was fixed with 12.0.0 by replacing the buffer data with a fixed-size-struct.

Stack infoleak with GetAdapterProperty, stack buffer overflow (and hence ROP) with SetAdapterProperty. 12.0.0 12.0.0 July 17, 2020 April 7, 2021 yellows8
bluetooth stack buffer overflow with HID DATA packets The BSA (bt-stack) func bta_hh_co_data copies data from a HID DATA packet to stack without checking the size, then sends it over Uipc. [7.0.0+] The user Uipc callback also copies the input data to stack without checking the size, then sends it to the sharedmem CircularBuffer.

With [12.0.2+] this was fixed in bta_hh_co_data by clamping the size to a maximum of 0x2BB. The aforementioned buffer overflow in the Uipc callback can't be triggered since at that point the size was already clamped.

Before this bta_hh_co_data func is reached, there is no validation of the size (such as comparing against the L2CAP MTU) when Basic Mode is being used.

Actually triggering this requires using a data-size larger than the normal L2CAP MTU. This can be done by for example, using raw HCI to send the packet from the remote bluetooth device.

Note that when the remote device is configured as an audio device for [12.0.0+] where BluetoothDevicesSettings.TrustedServices was only ever set for audio since system-boot, it is not possible for the remote device to connect to the Switch for HID.

ROP under bluetooth via HID DATA packet sent by a paired HID bluetooth device. This can be triggered at any time while not in sleep-mode, when not in airplane-mode. The earliest is while the Nintendo Switch logo screen is displayed during system boot. 12.0.2 12.0.2 July-August 2020 May 11, 2021 yellows8
bluetooth WriteHidData/WriteHidData2/SetHidReport unchecked memcpy size WriteHidData/SetHidReport copies the input struct to stack, then passes it to the funcptr/vfunc call. WriteHidData2 passes the input buffer addr directly to the funcptr/vfunc call. The called func eventually copies the input data to the stack struct using the specified size without validating it.

This requires access to the btdrv service, only hid and btm have access.

This was fixed with 12.1.0 in WriteHidData/SetHidReport by doing a fixed-size copy into another tmp struct, with the size field being clamped to a maximum of 0x2BB afterwards. This struct is then used when calling the vfunc. The vfuncs called by WriteHidData/WriteHidData2/SetHidReport were also updated to clamp the size to the required maximum value.

Stack buffer overflow 12.1.0 12.1.0 July 16, 2020 July 6, 2021 yellows8
Infoleak with hid:sys SetButtonConfigStorage{name}Deprecated These cmds pass a stack ptr for the StorageName when calling the internal func. Nothing is written to this StorageName. Hence, stack infoleak (data is copied as a NUL-terminated string), which can be later read by the GetButtonConfigStorage{name} cmds.

This was fixed by removing the Deprecated cmds in 13.0.0.

Infoleak of hid stack from a StorageName readable via GetButtonConfigStorage{name}, up to the NUL-terminator. 13.0.0 13.0.0 December 11, 2020 September 27, 2021 yellows8
bluetooth EventInfo infoleak The various funcs which send messages to the thread which handles writing to EventInfo, didn't clear the stack msgbuf. Hence, the various get-EventInfo cmds could return leaked stack data. This likely affected most (?) get-EventInfo cmds, besides CircularBuffer-GetHidReportEventInfo.

This only matters for events where there's uninitialized regions of the EventInfo, such as events with variable-size data without a memset.

This was fixed by clearing the msgbuf in a number of funcs.

Bluetooth-sysmodule stack infoleak, which allows defeating ASLR 13.0.0 13.1.0 During initial diff. Added to this page on: December 12, 2021 yellows8
ssl CVE-2021-43527 CVE-2021-43527, see also here and here.

Using BigSig where the server cert sig is RSA-PSS results in the remote server throwing {no shared cipher} error when Switch connects. If however one creates a rootCA using BigSig (RSA-PSS), which then signs a server cert where the server key is RSA (not PSS), the vuln can be triggered (if the rootCA is trusted, via using the import service-cmd). It's unknown whether there's other ways to trigger the vuln.

The crash occurs in VFY_Begin when using the previously overwritten data. A bitsize of $((16384 + 32 + 64 + 64 + 64)) is only enough to overwrite cx->hashcx, to fully overwrite cx->hashobj an additional 0xC-bytes (additional 96 bits) is needed. Note that partial overwrite isn't an option: this is the func that initializes those fields to begin with, it just does deinit first before initializing hashcx/hashobj (prior to that these fields would be all-zero when not overwritten by the buf-overflow).

Heap buffer overflow in ssl, overwriting data including a ptr to an object which is later used to load a funcptr. 13.2.1 13.2.1 Switch: December 1-2, 2021 Switch: January 19, 2022
bluetooth BSA gatt_process_notification stack buffer overflow gatt_process_notification is the GATT handler for processing notification/indication messages. gatt_process_notification does memcpy to stack from the input bt msg data, without size validation. The input len param isn't validated in this func either - if the remaining len following op_code is less than 2, a negative value will be used for the data copy to stack.

These were fixed by adding a bounds check for the size, size==0 is also checked for now.

Bluetooth-sysmodule stack buffer overflow, with data received from a bluetooth message 13.2.1 13.2.1 November 2021 January 19, 2022 yellows8
AM IDisplayController TakeScreenShotOfOwnLayer OOB The captureBuf is used as an array index without validation. Data used from this array includes calling a funcptr from the array entry, if set. Eventually this is also used to write bools into this array, one of which is from the command input.

With [5.0.0+] a func is eventually called to get a ptr determined by the input captureBuf, with nullptr being returned for captureBuf>=0x10. The caller will Abort if nullptr was returned.

OOB array access 5.0.0 13.1.0 ~July 31, 2019 January 26, 2022 yellows8
AM IDisplayController ClearCaptureBuffer OOB The captureBuf is used as an array index without proper validation. There is code validating it, but on failure it just skips over a code-block, with code using captureBuf still being used afterwards. Then this is used to write bools into a global array, one of which is from the command input.

This was fixed with [9.1.0+] by requiring captureBuf = 0-1.

OOB bool writes into an array 9.1.0 13.1.0 ~July 31, 2019 January 26, 2022 yellows8
bsdsockets ioctl SIOCGIFCONF infoleak Originally bsd ioctl SIOCGIFCONF was handled by setting the data in IPC outbuf0 to the size/addr of IPC outbuf1. These buffers are HipcAutoSelect, so if buf1 is small enough for HipcPointer (otherwise it would be HipcMapAlias) the IPC-buf-ptr leaked into outbuf0 would be located in the codebin-region. Since this is done before the actual ioctl-handling, it doesn't matter whether the fd is valid.

This was fixed in [5.0.0+] by using a tmp struct on stack instead of buf0.

bsdsockets-sysmodule codebin-region addr infoleak, which allows defeating ASLR. 5.0.0 13.1.0 February 14, 2022 (probably earlier) February 14, 2022 yellows8, probably others
bsdsockets ioctl SIOCGIFMEDIA input can contain ptr Originally bsd ioctl SIOCGIFMEDIA used the user-specified ifmediareq structure directly from the input buffer. This includes a ptr. This ptr probably isn't actually used?

With [5.0.0+] the structure used as input for the ioctl was changed to using int ifm_ulist[1] instead of int *ifm_ulist (which is unused). The input structure is copied to a tmp struct which is used as the original ifmediareq structure, with ifm_ulist always NULL. The user can still specify a non-zero ifm_count value, however that's not useful with ifm_ulist being always NULL.

Useless? 5.0.0 13.1.0 February 14, 2022 February 14, 2022 yellows8, probably others
Infoleak with Joy-Con HidCommand PairingIn The joycon protocol handler for PairingIn copies data from stack to the response cmd-buf for sending PairingOut. Only the first byte is set to a type value, the rest is uninitialized stack data.

This was fixed with [15.0.0+] by directly writing to the response data without using stack data.

Infoleak of hid stack via a bluetooth/uart message+response with a connected hid controller. This returns addrs for the main-codebin/stack, which allows defeating ASLR. 15.0.0 15.0.0 September 4, 2020 October 10, 2022 yellows8
Broken RNG for ro ASLR The RNG used to determine where to randomly map NROs in the target process was TinyMT (nn::os::detail::RngManager output, seeded by 128 bits of entropy). However, TinyMT is not cryptographically secure (and can in fact be analytically solved).

Thus, with a few NRO mapping addresses, one could learn the TinyMT state and derive all previous/future RNG outputs, breaking NRO aslr for all processes.

With [15.0.0+] ro now uses csrng_GenerateRandomBytes to determine the random map address for NROs.

Breaking ASLR for all NROs loaded in all processes, allowing predicting all NRO mappings for all processes until the next reboot. 15.0.0 15.0.0 Late 2021/Early 2022 October 11, 2022 Everyone
Broken RNG used by ns The code generating the sd seed and the data for the sd private/private1 file, all use nn::os::GenerateRandomBytes, not csrng. The sd-seed is generated first, then private, then private1. This allows deriving sd-seed from private since this uses TinyMT, as long as the system shipped from factory on [2.0.0+]. private1 is only useful if the system shipped with [4.0.0+].

There's various other code in ns using nn::os::GenerateRandomBytes as well. This includes the code generating ns_systemseed when it doesn't exist. ns_systemseed is generated at some point after the various sd-seed-related code (both are called from the same func). Hence, ns_systemseed can be recovered with the above method as well, if it wasn't recreated at some point without regenerating the above nand-save used with the above.

With [15.0.0+] ns now uses csrng_GenerateRandomBytes for sd-seed/private and ns_systemseed, etc. This only matters when the file is newly generated, which is usually only for factory-fresh systems which ship with this version. This would also apply after being deleted during {System Settings -> Formatting Options -> Initialize Console}, and also with a refurbished console.

Generation of a system's sd-seed allowing decryption of the NAX0 layer of data on SD, derived using the private file from SD. Applies to systems which factory-shipped with a system-version prior to 15.0.0 (that is, [2.0.0-14.1.2]). 15.0.0, for newly generated files 15.0.0 December ~12, 2021 October 11, 2022 yellows8
bluetooth BSA bsa_sv_av_cback stack buffer overflow bsa_sv_av_cback checks for two input type values (0xC/0xD), on match it copies the input data to stack without size validation. Then it sends an internal request with this data (likewise when the type values don't match, except the input data is passed directly with a small size), then it returns.

This requires the AV functionality added with [13.0.0+], however this func is only reachable with [14.0.0+] where the required functionality was enabled.

This requires message data that's larger than the MTU, so fragmentation must be used, or manually send the ACL data to bypass the MTU.

This can be triggered via an AVRC message with opcode=0x0 (vendor). The above type 0xC is reached via AVRC ctype 0..4, while 0xD is reached with ctype>=0x9.

With [15.0.0+] the size value for the memcpy (which is also written to the request struct) is clamped to a max value.

Bluetooth-sysmodule stack buffer overflow on [14.0.0-14.1.2], with data received from an AVRC bluetooth message with a bluetooth-audio device. 15.0.0 15.0.0 November 2021 October 11, 2022 yellows8
wlan SetMulticastList heap buffer overflow The SetMulticastList command allocates a 0x31-bytes sized buffer and copies to it as much MacAddress values from the input MulticastList as specified by the "Count" field, but this field is never validated.

With [15.0.0+] error code 0x1906B is now returned if "Count" is larger than 8.

wlan-sysmodule heap buffer overflow. 15.0.0 15.0.0 June 6, 2022 November 9, 2022 hexkyz
bluetooth WriteGattCharacteristic/WriteGattDescriptor stack buffer overflow regression Originally btdrv WriteGattCharacteristic/WriteGattDescriptor (bt service LeClientWriteCharacteristic/LeClientWriteDescriptor are the same) validated the input buffer size. However the size check was removed with [12.0.0+] (which was also when bluetooth was refactored), hence stack buffer overflow. Anything with btdrv/bt services access can trigger it. While this is intended to require a BLE connection, it seems to be possible to trigger the buffer overflow without any BLE connection by passing ConnectionHandle=0xFFFFFFFF (handle not tested on hardware). Bluetooth-sysmodule stack buffer overflow on [12.0.0-15.0.1], with data from BLE IPC cmds. 16.0.0 16.0.0 December 10, 2021 February 23, 2023 yellows8
JIT usability issues CreateJitEnvironment will enter infinite-loops using nn::jitsrv::detail::AslrAllocator::GetAslrRegion when either of the input CodeMemory sizes are zero. Also the second CodeMemory is useless for the user-process since the second addr returned by GetCodeAddress is a dup of the first one, set during state init by CreateJitEnvironment.

With [14.0.0+] size=0 is now properly handled, and also the state for the second addr from GetCodeAddress is now properly initialized.

Minor usability issues, not useful for exploitation (size=0 will cause jit-sysmodule to hang in a loop). 14.0.0 14.0.0 October 1, 2020 February 26, 2023 yellows8
usbhs uninitialized IClientEpSession usbhs IClientIfSession OpenUsbEp creates an IClientEpSession object. The allocated object from ExpHeap is not memset, only select fields are cleared. The rest of initialization is done by PopulateRing - however the user-process could skip using that if wanted (official sw always uses it).

ShareReportRing maps tmem and writes the ring buffer/count field into object state. PopulateRing also eventually initializes these fields, with the buffer being allocated from ExpHeap instead of tmem. These fields are not cleared during object creation from OpenUsbEp.

GetXferReport after validating the cmd input, just uses object state assuming it was initialized. This runs code which is the same as the user-process code handling the tmem ringbuf.

Therefore, by skipping using PopulateRing and then using GetXferReport the sysmodule will use an uninitialized ringbuf ptr, and an uninitialized count field. If one could control these fields by doing ExpHeap allocations prior to OpenUsbEp so that {target fields} would be located at {IClientEpSession ring fields}, then one could read usb-sysmodule memory at the target buffer address.

See here for ringbuf format. The sysmodule will Abort if read_index is >= {ring count field from object state}. Otherwise it copies an entry from that index to output, and updates read_index.

This is probably tricky to abuse as the ringbuf ptr has to be valid, and {see above} (likewise for write_index when the report-ringbuf-writing func runs).

PostBufferAsync/BatchBufferAsync also use seperate object ring fields which are left uninitialized from OpenUsbEp. Targeting this would be tricky with the ring restrictions - this would allow writing data to a ring addr however.

Pre-4.0.0 (only 2.0.0 checked) is not affected by these. The ring fields in the object are cleared during object creation (no memset of the entire object however). GetXferReport would null-deref if PopulateRing was skipped. PostBufferAsync/BatchBufferAsync will throw an error if PopulateRing was skipped. Pre-4.0.0 also has different ring handling as well.

[16.0.0+] The IClientEpSession init func now clears the remaining previously uninitialized fields. The cmds using the ring fields still don't check for NULL, so using GetXferReport/PostBufferAsync/BatchBufferAsync without PopulateRing will just trigger null-deref. Even if the ptr were somehow valid but ring-count field was left at 0, this would then Abort due to: if (ring_count <= index_loaded_from_ringptr) <Abort>

[4.0.0-15.0.1] If one can trigger using {target values} as the unintialized fields: memory reads from the target addr with GetXferReport, and memory R/W with PostBufferAsync/BatchBufferAsync. This requires access to usb:hs, and an usb device must be connected which is not being used by {other sessions}. If successful, this might (?) result in usb-sysmodule compromise. 16.0.0 16.0.0 January 30, 2023 February 26, 2023 yellows8
ns RequestMoveApplicationEntity/EstimateSizeToMove buffer overflow ns RequestMoveApplicationEntity eventually calls a func which: Loops through the input buffer. If any entry has value 6, it will call another func to copy data from state to output safely (uses the max_count param). Otherwise, it copies the input buffer to an outbuf (located on caller's stack) without any size validation (inlined memcpy), even though there is a max_count param.

Additional memwrites are also done to the above outbuf following the initial memcopy. This can be avoided if the buffer doesn't contain bytes with values 3-6 (if using values in that range is really needed, the cmd input StorageId param can be set to the required value so that the specified value doesn't trigger the memwrite). Value 6 shouldn't be used anyway (see above).

ns EstimateSizeToMove first calls the same func which does the copy above (outbuf is also located on stack), then it calls another func. Hence, same vuln here.

By corrupting just the first byte of x29 with EstimateSizeToMove, one can obtain infoleaks. This method with x29 essentially only works with [15.0.0+]. Pre-15.0.0 would require a different method with partial overwrite of retaddr, however it's unknown whether this would actually work for infoleak (would require [12.0.0+] for the stack layout change). With EstimateSizeToMove where x29 is overwritten, the output u64 is the leaked ptr (can be codebin-region). Note that the cmd has to return Result=0 for this to work. x29 is used to load the value which is copied to the cmdreply rawdata.

As of [17.0.0+] an error is thrown if the input array count is larger than 8 (size of the stack dst-array).

ns-sysmodule stack buffer overflow, allowing ns infoleak+ROP. 17.0.0 17.0.0 January 2, 2023 October 17, 2023 yellows8
ovln:snd OpenSender unvalidated count ovln:snd OpenSender has a count param. This count is used to allocate the specified number of objects in a linked-list for storing the data from Send. If count is 0, the linked-list is left empty, with ptrs to itself within the ISender object.

ISender Send when the above linked-list is empty, runs a switch-statement with (inval>>8)&0xFF. This uses another linked-list where the ptrs are initially {within ISender obj}. No space is allocated in the ISender obj for the linked-list object-data. Therefore using Send with val 1<<8 or 2<<8 (other values throw error) results in the specified input struct being copied into the ISender obj, which then overwrites heap data OOB. If for example one used OpenSender again right after the first OpenSender usage, then used Send as described above, this would corrupt the second ISender which includes overwriting the vtable. If one would use Send twice in a row like this, the second one would use a corrupted linked-list (written from the first Send). If the linked-list ptrs would be valid (no crash triggered) this would allow one to copy the input data to a controlled addr, though it's restricted with the linked-list usage.

Using GetUnreceivedMessageCount afterwards is of no interest.

Besides ovln, the only other allocs on this heap is from IPmModule Initialize. This heap is also used for psc:* services (object allocs).

In theory (untested) it may be possible to also use this to obtain infoleaks, however it would only return the high-u32 of ptrs not the low u32. Essentially, one would trigger object allocations so that ExpHeap has layout: {ISender} -> {RF chunk from freeing an object} -> {module object from IPmModule Initialize}. Then one would use the Send vuln to corrupt the RF chunk, changing the size to a larger value. Then one would trigger an object allocation (probably same object which was previously freed), then another object for overwriting the module object (ISender would work) with ptrs at the target offsets in the module object. Then once IPmModule GetRequest is used, the returned u32s would be the high-u32 from ptrs. Due to alignment requirements with each allocation, it isn't possible to shift the allocations in order to leak ptr low-u32.

[17.0.0+] Now throws an error if the input count for OpenSender is 0.

psc-sysmodule heap memory corruption (ns-sysmodule on pre-8.0.0). 17.0.0 17.0.0 January 13, 2023 October 20, 2023 yellows8
nv NVGPU_GPU_IOCTL_GET_CHARACTERISTICS Ioctl3 infoleak The handler code for NVGPU_GPU_IOCTL_GET_CHARACTERISTICS for Ioctl/Ioctl3 are essentially the same, except for the value used for the max-size clamp: Ioctl uses constant 0xA0, while Ioctl3 uses the outbuf1_size. So if one uses this with Ioctl3 and a large outbuf1, this will memcpy data OOB from the source buffer, hence infoleak.

With [17.0.0+] the second block of csel code which previouly essentially used the clamped size from above, was replaced with code which properly clamps to the max-size constant.

nvservices-sysmodule infoleak, which allows defeating ASLR. 17.0.0 17.0.0 February 25, 2022 October 24, 2023 yellows8
audctl GetTargetDeviceInfo infoleak audctl GetTargetDeviceInfo calls an impl func with a ptr to a stackbuf, then if successful memcpys the 0x100-bytes from that buffer to output. This stackbuf is not memset. This func (after doing various state checks) copies a string to output, other than always writing a NUL-terminator there's no clearing of the buffer.

This will leak audio-sysmodule stack into the output buffer as long as the state/input checks pass (for the remainder of the buffer following the string NUL-terminator).

With [18.0.0+] data is written directly to the outbuf instead of the stack tmpbuf.

audio-sysmodule infoleak, which allows defeating ASLR. 18.0.0 18.0.0 December 24, 2022 March 26, 2024 yellows8
audctl GetSystemInformationForDebug infoleak / buffer overflow audctl GetSystemInformationForDebug calls a func with a 0x1000-byte stack tmpbuf, then afterwards that buffer is memcpy'd into the cmd outbuf. This called func doesn't clear the buffer. This func eventually uses btm cmd75 with outarray={global ptr} and count=10. Then if the outcount is s32 >=1, it loops through the output using the outcount, without validating it besides the <1 check. Data from that outarray is copied into the array in the func output buffer (tmpbuf above).

With btm comprimised, one could return a large output count and trigger a stack buffer overflow with data following that global array, however exploiting this would be difficult since that data would be uncontrolled (can't directly control it from this cmd at least).

A stack infoleak can be obtained with this as well (assuming the above output array isn't full).

Even though the name has "ForDebug", there's no checks which would trigger an error / return early (this also always returns 0).

[18.0.0+] now clears the output buffer, and also now prints strings into the buffer instead of writing binary data (overflow no longer possible).

audio-sysmodule infoleak, which allows defeating ASLR. Also audio-sysmodule memory corruption, likely not useful unless there's a way to control the data. 18.0.0 18.0.0 December 7, 2022 March 27, 2024 yellows8

Internet Browser

Summary Description Successful exploitation result Fixed in system version Last system version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
CVE-2016-4657 WebKit vuln discovered around August 2016. Most notably used in the iOS 9.3.X exploit. A simple PoC can be found here. This was later exploited by Qwertyoruiop using an adjusted version of his iOS 9.3 webkit exploit (others exploited this prior to then). 2.1.0 2.0.0 Original: August 2016

Switch: March 3rd-4th 2017

Everyone
CVE-2017-7005 WebKit type confusion. 3.0.1 3.0.1 Everyone
CVE-2016-4622 WebKit memory corruption bug. This bug was incorrectly re-introduced in 4.0.0. See here for a detailed write-up from the author. 6.1.0 6.1.0 Everyone
CVE-2018-4441 WebKit memory corruption bug. See here. 7.0.0 7.0.0 Everyone

Whitelist

This section documents WebApplet whitelist issues in applications. These can be used to load your own browser content over plain HTTP, which then for example could be used for web-applet exploitation.

Application Description Fixed with app version Newest app version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
Sonic Mania Originally this game launched web-applet with a plain-http URL for displaying the manual, this was later changed to https. Originally the whitelist only had 1 entry for a http URL, this was later replaced with various https-only URLs. 1.04, unknown if fixed with an earlier update 1.04 January (?) 2022 February 23, 2022 yellows8

NintendoSDK

This section documents vulnerabilities for NSOs in NintendoSDK.

nnSdk

This section documents vulnerabilities for nnSdk (sdknso).

Summary Description Successful exploitation result Fixed in SDK version Last SDK version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
hidbus GetJoyPollingReceivedData buffer overflow hidbus GetJoyPollingReceivedData doesn't validate the u8 size used for memcpy, when copying the data to the output JoyPollingReceivedData. With 11.x, the size is now clamped to a maximum of 0x2C (regardless of polling-mode). Note that 0x2C is the data-size for JoyButtonOnlyPollingDataAccessor, the other polling-modes have a smaller size.

The hid-sysmodule code which writes data here does handle it properly: size is clamped to a max size, and the data-read uses a fixed-size anyway (hence there's no way to trigger this sdknso vuln with the hid-sysmodule tmem writing code).

This could only be exploited if one directly writes to the tmem when one has previously compromised hid-sysmodule, without using the normal tmem-writing func for this.

There are only a few apps which use hidbus.

Triggering a buffer overflow in an application which uses hidbus GetJoyPollingReceivedData, from a previously compromised hid-sysmodule. 11.x.0 11.4.0 March 2020 December 3, 2020 yellows8
Profile Selector uninitialized input data Originally unused regions of Profile_Selector UiSettings/UserSelectionSettings were not cleared prior to being sent to the applet. With 1.x.x these are now properly memset(). Stack infoleak from user-process, sent to the applet. 1.x.x 11.4.0 November-December 2019 December 31, 2020 yellows8

Pia

This section documents vulnerabilities for Pia.

In v5.11.3 (exact starting version unknown) the fixes aren't present for the below vulns which were fixed in v5.9.3, while in v5.18.98 these are present (exact starting version unknown). This probably indicates that the vuln fixes were backported from a newer Pia version to v5.9.3.

The Pia packet handlers are only active when the game is using multiplayer. LanProtocol is only active in the games which are actively using the LAN-mode option (not Ldn) - only certain games support LAN-mode. The LanProtocol Pia packet handler can be reached while in a lobby or searching for one.

Most Pia packets require an active StationProtocol connection to be active with {InetAddr which the packet was received from}, otherwise the packet is filtered out. The only protocols which don't use filtering are the following: NatTraversalProtocol, LanProtocol, StationProtocol, LocalProtocol.

Note that broadcast IP-dest Pia packets are accepted - this can be used to target every device on the network which is using Pia (which is really only useful with {above protocols} due to the filtering mentioned above, unless one also handles StationProtocol).

Summary Description Successful exploitation result Fixed in Pia version Last Pia version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
nn::pia::session::RelayRouteManageJob::UpdateConnectionReport buffer overflow nn::pia::session::RelayRouteManageJob::UpdateConnectionReport() checks that the input size is at least {value}, but there's no max size check. This is used to memcpy from the input to elsewhere - hence buf-overflow if size is too large. The dst buffer is allocated on the pead heap - this buffer is probably small.

Note that there's various requirements before it would actually reach the memcpy, such as <nn::pia::session::Mesh::IsHost() const> must return true.

In fixed versions immediately after the StationIndex validation it now does: if(statefield+0x10<input_size) return;

This is called from nn::pia::session::MeshProtocol::ParseConnectionReport().

Heap buffer overflow triggered by a Pia MeshProtocol message sent to a host device. v5.9.3, see above. v5.9.1/v5.9.2/v5.9.3 November 11, 2022 November 15, 2022 yellows8
nn::pia::lan::LanProtocol::ParseSessionMessage buffer overflow nn::pia::lan::LanProtocol::ParseSessionMessage() calls nn::pia::lan::LanSessionMessage::Deserialize() to deserialize the message payload data buffer into the LanSessionMessage object on stack. LanSessionMessage::Deserialize (among other things) memcpys data from the input buffer to the object, using an u32 from the input buffer - there is no size validation in Deserialize itself.

There is a size check immediately after calling Deserialize() to verify payloadsize=={u32val}+{constant}, returning on fail - but this doesn't matter for too-large-size.

In fixed versions Deserialize now does bounds checking, both for the minimum message size and clamping the memcpy size to a constant. An error is thrown if the clamped memcpy size is larger than the message size. The caller now checks the ret properly, previously it was ignored.

Following the size check in ParseSessionMessage() it calls <nn::pia::session::Mesh::IsProcessingLeaveMesh() const>, returning if ret is false.

Then it calls nn::pia::lan::LanProtocol::ReceivedFragmentData::Receive(), with the memcpy'd buffer/size from the above LanSessionMessage, and other fields from LanSessionMessage. This eventually memcpys the input buffer to object+{offset}+{chunksize_field}*inputu8, there is no validation for size or inputu8 (except for the above size check). Hence, if the u8 is large enough, this would result in a heap buffer overflow.

In fixed versions ReceivedFragmentData::Receive added a bunch of validation before the memcpy.

Stack/heap buffer overflow triggered by a Pia LanProtocol message. v5.9.3, see above. v5.9.1/v5.9.2/v5.9.3 November 14, 2022 November 15, 2022 yellows8
nn::pia::session::SessionProtocol::ParseLeaveMeshInvitation buffer overflow <nn::pia::session::SessionProtocol::ParseLeaveMeshInvitation(nn::pia::transport::ReceivedMessageAccessor const&)> This immediately returns if *(ReceivedMessageAccessor+16) is 0. Then the input data is deserialized. The input u64 array is deserialized to stack, the u8 arraycount field from input is not validated.

Hence, stack buffer overflow. Note that there's similar loop code in nearby funcs, which do validate the count properly.

In fixed versions the arraycount field is now validated.

SessionProtocol uses ReliableSlidingWindow MessageHeader, with a maximum message size of 0x100. The allocated size used for the above u64 array is also 0x100-bytes. Hence, when triggering a buf overflow the data after the buffer is uncontrolled data from the SessionProtocol object.

Stack buffer overflow triggered by a Pia SessionProtocol message. v5.9.3, see above. v5.9.1/v5.9.2/v5.9.3 November 14, 2022 November 15, 2022 yellows8
Optional Pia packet encryption Pia packet encryption is optional. If the encryption flag is disabled, the packet handler will accept it and skip crypto.

In fixed versions immediately after grabbing a packet, it now checks the crypto flag. If it's plaintext the packet is dropped.

This can be used to send a plaintext Pia packet without needing to handle encryption, especially useful if the session-key can't be obtained (online-play matchmaking). This could be combined with other vulns if wanted.

Sending a plaintext Pia packet without needing to handle encryption. v5.9.3, see above. v5.9.3 (and later versions) November 19, 2022
nn::pia::session::{JoinMeshJob/ProcessUpdateMeshJob}::SetStationDataList OOB read/write/vfunc-call nn::pia::session::JoinMeshJob::SetStationDataListis called by nn::pia::session::MeshProtocol::ParseJoinResponse(nn::pia::transport::ReceivedMessageAccessor const&)> with the ReceivedMessageAccessor buffer.

SetStationDataList will update state and immediately return if the join was denied. It will also validate the num_mesh_stations field against state. ParseJoinResponse also essentially verifies that the message was received from the host device.

The input buffer size is ignored.

The num_fragments field must be value 1 or <=3 otherwise it will return, there's two seperate code blocks handling these.

Other than the checks at the start, there's no validation for the index fields. So large enough values could result in OOB-reads.

When handling multiple fragments, it will loop through the stationinfo list. There is no validation for the u8 count field or the baseindex field. It calls a vfunc from obj baseptr+index*{entrysize} with data from the buffer, where index starts with the above baseindex field. Afterwards, an u8 is copied into an u32 array (with certain versions an u16 is deserialized into an u16 array).

nn::pia::session::ProcessUpdateMeshJob::UpdateStationDataList is (eventually) called from nn::pia::session::MeshProtocol::ParseUpdateMesh, which has similar issues to the above.

Note that ParseJoinResponse/ParseUpdateMesh essentially require the message to be received from the host device.

With fixed versions (v5.18.98, exact version unknown) various validation was added. Additional/updated validation was added in a later version (v5.31.0, exact version unknown).

OOB read/write / vfunc call where the object is selected by an OOB index, triggered by a Pia MeshProtocol message. v5.18.98 and v5.31.0 (exact versions unknown). v5.31.0 November 18, 2022 November 21, 2022 yellows8
Insecure encryption Originally Pia packets used AES-ECB encryption. As documented here it was later changed with v5.7.0 to AES-GCM. Each 0x10-byte block would have the same encrypted block output where the plaintext 0x10-byte data is the same.

The mechanism for generating the Pia SessionKey for LAN has also changed over time.

The LAN non-Pia-encapsulated packets were also originally sent in plaintext, however at some point it was changed to mostly encrypted.

AES-GCM fix: v5.7.0
nn::pia::transport::UnreliableProtocol::Dispatch buffer overflow nn::pia::transport::UnreliableProtocol::Dispatch memcpys data from the message into a list entry, without size validation. If the pia packet is the max size, it will only overwrite the 0xC-bytes which were written to immediately before the memcpy: the u32 size and the 8-byte StationAddress (depending on the version there can also be 4-byte padding after the size for alignment).

However, nn::pia::transport::UnreliableProtocol::Receive will clamp the size from the list entry to the outbuf size when doing the memcpy. So this is probably useless.

It's unknown whether there's a version where more data could be overwritten, and whether that would be useful.

This is fixed in v5.31.0, exact version unknown. The message is dropped if too large in Dispatch.

Small buffer overflow triggered by a Pia UnreliableProtocol message. v5.31.0, exact version unknown. v5.18.98/v5.31.0 November 2022 November 29, 2022 yellows8
Uncleared input structs for LDN The Pia code using ldn CreateNetwork*/ConnectNetwork*/Scan doesn't properly memset the input data for SecurityConfig/ScanFilter (when keysize is less than 0x40 for the former). Hence, infoleak from games is sent to ldn (structs are located on stack, so stack data is leaked). This requires ldn compromise/mitm to obtain the leaked data - these are not sent over the network.

With v6.20.1 (exact version unknown - fix isn't present in v5.32.0), the code using Scan* now clears the input ScanFilter properly. With v6.25.1 (exact version unknown - fix isn't present in v6.23.3), the code using CreateNetwork*/ConnectNetwork* now clears the input SecurityConfig properly.

Infoleak from games with LDN cmds, requires compromised sysmodule/mitm. v6.20.1 and v6.25.1, exact versions unknown. v5.32.0/v6.20.1/v6.23.3/v6.25.1 December 7, 2022

ENL

This section documents vulnerabilities for ENL. A framework used by Nintendo games including Mario Kart 8 Deluxe, Splatoon 2 / 3, Mario Maker 2, and more.

Fun fact, this library appears to re-use network code and concepts from older Nintendo titles such as Mario Kart 7 and some Wii multiplayer games.


Summary Description Successful exploitation result Fixed in Enl version Last Enl version this flaw was checked for Timeframe this was discovered Public disclosure timeframe Discovered by
enl::TransportManager::updateReceiveBuffer_() nullptr deref enl::TransportManager::updateReceiveBuffer_() is called when the ENL framework receives a PIA packet from a client, it will fully trust the ENL header which includes a "ContentTransporter" type (ID) and a length.

The function will try to fetch the content transporter by ID using enl::TransportManager::getContentTransporter(unsigned char const &), it returns NULL if there's no content transporter with the same ID

  • NOTE: The function may be inlined

Then it will try to call a virtual method: virtual size_t readyReceiveStream(enl::RamReadStream&, enl::Buffer*, size_t), dereferencing the pointer to fetch the vtable ptr

Pseudocode of the function before it was fixed

nullptr dereference triggered by an invalid content transporter type in the ENL header (it will crash the game/process) Unknown Depends on the game Early April 2022 November 16, 2022 Rambo6Glaz, Kinnay (massive RE help)

There's another one more interesting but it will have to wait a bit :)