{"article":{"slug":"when-the-debugger-lies","title":"When the Debugger Lies","subtitle":null,"summary":"Daniel Mangum digs into unexpected register values on Nordic's nRF54L while poking the Key Management Unit from a debugger, and explains how SoC security components can make the debugger's view of memory misleading.","content_type":"blog_post","language":"en","canonical_url":"https://danielmangum.com/posts/when-the-debugger-lies/","author":{"name":"Daniel Mangum","url":"https://danielmangum.com/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Daniel Mangum","url":"https://danielmangum.com/","listing_slug":null,"listing":null},"topics":[{"name":"Engineering","slug":"engineering","url":"https://listedarticles.com/topics/engineering"},{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Hardware","slug":"hardware","url":"https://listedarticles.com/topics/hardware"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2579,"reading_minutes":11,"published_at":"2026-09-18T00:00:00.000Z","added_at":"2026-09-24T12:25:52.022Z","updated_at":"2026-09-24T12:25:52.022Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/when-the-debugger-lies","markdown_url":"https://listedarticles.com/articles/when-the-debugger-lies.md","example":false,"citation":"Daniel Mangum, Daniel Mangum. \"When the Debugger Lies.\" 18 Sept 2026. https://danielmangum.com/posts/when-the-debugger-lies/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://danielmangum.com/posts/when-the-debugger-lies/"},"body_markdown":"I’ve spent the last few weeks working with the security architecture of the nRF54L series from Nordic Semiconductor (in case you missed it, I recently joined Nordic!). While doing so, I have engaged my typical low-level learning technique of eschewing writing firmware for manually poking at registers using the debugger. A few nights ago I found myself observing unexpected values in memory when working with the key management unit. It turned out to be a familiar issue, but one that requires an understanding of the internal system on chip (SoC) components, and how the debugger interacts with them, to diagnose.\n\nFor a bit of background, the nRF54L series has a fairly advanced set of security capabilities, headlined by Arm TrustZone support in the Cortex-M33 core, a CRACEN cryptographic accelerator, and a Key Management Unit (KMU). The KMU is used for storing sensitive data, such as key seeds and associated metadata, in the Secure Information Configuration Region (SICR).\n\nThe SICR is divided into slots. These slots are targeted by issuing tasks to the\nKMU, which can only be accessed in secure mode. Typically, application firmware\ndoesn’t interact with the KMU directly. Instead, PSA\ndrivers are implemented\nto abstract the generation, storage, and usage of keys. For example, if\ninvoking\n`psa_generate_key()`,\nthe operation eventually results in a call to\n`import_key_for_kmu()`\nin the CRACEN PSA\ndriver.\n\n```\nstatic psa_status_t import_key_for_kmu(const psa_key_attributes_t *attributes, const uint8_t *data,\n\t\t\t\t       size_t data_length, uint8_t *key_buffer,\n\t\t\t\t       size_t key_buffer_size, size_t *key_buffer_length,\n\t\t\t\t       size_t *key_bits)\n{\n\tsize_t opaque_key_size;\n\tpsa_status_t status = PSA_ERROR_CORRUPTION_DETECTED;\n\tint slot_id =\n\t\tCRACEN_PSA_GET_KMU_SLOT(MBEDTLS_SVC_KEY_ID_GET_KEY_ID(psa_get_key_id(attributes)));\n\tpsa_key_attributes_t stored_attributes;\n\tstatus = cracen_get_opaque_size(attributes, &opaque_key_size);\n\tif (status != PSA_SUCCESS) {\n\t\treturn status;\n\t}\n\tif (key_buffer_size < opaque_key_size) {\n\t\treturn PSA_ERROR_BUFFER_TOO_SMALL;\n\t}\n\tstatus = cracen_kmu_provision(attributes, slot_id, data, data_length);\n\tif (status != PSA_SUCCESS) {\n\t\treturn status;\n\t}\n\tstatus = cracen_kmu_get_builtin_key(slot_id, &stored_attributes, key_buffer,\n\t\t\t\t\t\tkey_buffer_size, key_buffer_length);\n\tif (status != PSA_SUCCESS) {\n\t\treturn status;\n\t}\n\t*key_bits = psa_get_key_bits(&stored_attributes);\n\treturn status;\n}\n```\nA slot can either be erased, provisioned, or revoked. The datasheet includes a helpful diagram of the state machine.\n\nSlots have an ID (`0` - `249`) and can store metadata (32 bits), a destination\naddress (32 bits), a value (128 bits), and a revocation policy (2 bits). The\nlatter determines how state changes when a key is in the provisioned state and\nvarious tasks that reference its slot ID are issued to the KMU. When\nexperimenting with the KMU, it is easiest to use the `ROTATING` revocation\npolicy, which dictates that the slot transitions back to the erased state when a\nrevoke task is issued.\n\nWhen the `PUSH` task is issued, the value in the slot is written to the\ndestination address that was specified when the key was provisioned. The\nprovisioning process is documented in the\ndatasheet,\nbut it can also be seen in the `cracen_kmu_key_slot_provision()`\nimplementation:\n\n```\nstatic int cracen_kmu_key_slot_provision(const nrfx_kmu_key_slot_data_t *key_slot_data,\n\t\t\t\t\t uint32_t slot_id)\n{\n\tint kmu_status;\n\tuint8_t orig_write_buf_size;\n\tcracen_kmu_key_slot_provision_write_enable_set(true, &orig_write_buf_size);\n\tkmu_status = nrfx_kmu_key_slot_provision(key_slot_data, slot_id);\n\tcracen_kmu_key_slot_provision_write_enable_set(false, &orig_write_buf_size);\n\treturn kmu_status;\n}\n```\nThe `nrfx_kmu_key_slot_data_t` definition can be found in the Nordic Zephyr\nHardware Abstraction Layer\n(HAL).\n\n```\ntypedef struct __PACKED\n{\n    uint32_t                     keyslot_value[KEY_SLOT_WORDS_COUNT]; ///< Key data to be provisioned.\n#if NRF_KMU_HAS_REVOKE_POLICY || defined(__NRFX_DOXYGEN__)\n    uint32_t                     revoke_policy;                       /**< Key revoke policy.\n                                                                       *   @ref nrfx_kmu_rpolicy_t\n                                                                       *   holds possible values. */\n#endif\n    uint32_t                     keyslot_dest;                        /**< Key slot destination when\n                                                                       *   performing key push. */\n#if NRF_KMU_HAS_METADATA || defined(__NRFX_DOXYGEN__)\n    nrfx_kmu_key_slot_metadata_t metadata;                            ///< Metadata to write to keyslot.\n#endif\n} nrfx_kmu_key_slot_data_t;\n```\nYou could write some fairly straightforward firmware, or even use the CRACEN KMU sample, build it, then flash it onto a development kit to easily provision a key to the KMU. However, if using the supported drivers (which you absolutely should), additional restrictions are placed on the values that you can use when provisioning a key slot. For example, while the KMU supports any 32 bit value for metadata, the PSA driver assigns meaning to each of the bits.\n\n```\ntypedef struct kmu_metadata {\n\tuint32_t metadata_version: 4;\n\tuint32_t key_usage_scheme: 2;\n\tuint32_t reserved: 8;\n\tuint32_t algorithm: 6;\n\tuint32_t size: 3;\n\tuint32_t rpolicy: 2;\n\tuint32_t usage_flags: 7;\n} kmu_metadata;\n```\nSimilarly, there are restrictions on the values that you can write and the destination to which a given type of key is pushed. If manually interacting with the KMU, the metadata, value, and destination are much more flexible. However, if you provision non-conformant data into slots in the KMU, then attempt to interact with it using the supported drivers, you are going to have a bad time.\n\nKnowing the risks, and that I could restore the SICR on nRF54LM20\nDK with\nan\n`ERASEALL`\noperation on the control access port\n(CTRL-AP),\nI had powered up the board and connected with\nGDB. As previously mentioned, the\nKMU can only be accessed in secure mode. However, when access port protection\nis not\nenabled,\nthe Secure Privileged Invasive Debug Enable\n(SPIDEN)\nsignal is driven high, and the on-board J-Link debugger (J-Link\nOB) can\noperate with secure privileges.\n\nWith the CPU halted, I tested that I was able to access the KMU and determined\nthat it was ready for operations by reading from the `STATUS`\nregister\n(`0x50049400`).\n\n```\n(gdb) x/1xw 0x50049400\n```\n```\n0x50049400:\t0x00000000\n```\nTo test the actual functionality, I followed the\nprovisioning\nand\npush\nsteps described in the datasheet. The first step was to build the `SRC` data\nstructure in memory, which is of the format specified in the packed\n`nrfx_kmu_key_slot_data_t` struct definition. To make testing multiple values\nsimpler, I wrote a tiny Python script to build the struct.\n\n```\nimport struct\nopen(\"kmu_src.bin\", \"wb\").write(\n    bytes.fromhex(\"abc123abc123abc123abc123abc123ab\")  # Value\n    + struct.pack(\n        \"<III\",\n        3,  # Revocation Policy\n        0x20000000,  # Destination Address\n        0xDEF678DE,  # Metadata\n    )\n)\n```\nThe produced `kmu_src.bin` contained the following contents.\n\n```\nxxd kmu_src.bin\n```\n```\n00000000: abc1 23ab c123 abc1 23ab c123 abc1 23ab  ..#..#..#..#..#.\n00000010: 0300 0000 0000 0020 de78 f6de            ....... .x..\n```\nTo write the data to memory, I used the GDB `restore` command.\n\n```\n(gdb) restore kmu_src.bin binary 0x20001000\n```\nThe next step was to write the address of the struct (`0x20001000`) to the KMU\n`SRC`\nregister\n(`0x50049504`) and specify the desired key slot (`9`) in the `KEY_SLOT`\nregister\n(`0x50049500`).\n\n```\n(gdb) set *(unsigned int*)(0x50049504) = 0x20001000\n```\n```\n(gdb) set *(unsigned int*)(0x50049500) = 9\n```\nBefore actually issuing the task, the resistive random access memory controller\n(RRAMC)\nmust be configured to allow unbuffered writes. I stored the current RRAMC\n`CONFIG`\nregister\n(`0x5004e500`) in a variable to be restored after completion of the task, then\nwrote the value `1`, which sets write enable (`WEN`) field to `1` and the buffer\nsize (`WRITEBUFSIZE`) to `0` (unbuffered).\n\n```\n(gdb) set $rramc_config = *(unsigned int*)(0x5004e500)\n(gdb) set *(unsigned int*)(0x5004e500) = 1\n```\nFinally, I wrote `1` to the `TASKS_PROVISION`\nregister\n(`0x50049000`), instructing the KMU to store the value and its metadata in key\nslot `9`. I verified the event was generated by subsequently reading the\n`EVENTS_PROVISIONED`\nregister\n(`0x50049100`).\n\n```\n(gdb) set *(unsigned int*)(0x50049000) = 1\n```\n```\n(gdb) x/1wx 0x50049100\n```\n```\n0x50049100:\t0x00000001\n```\nWith the task completed, I then reset the RRAMC `CONFIG` register.\n\n```\n(gdb) set *(unsigned int*)(0x5004e500) = $rramc_config\n```\nThese exact operations can also be seen in the CRACEN PSA\ndriver’s\n`cracen_kmu_key_slot_provision()` and the underlying\n`nrfx_kmu_key_slot_provision()`\nfunction.\n\n```\nstatic int cracen_kmu_key_slot_provision(const nrfx_kmu_key_slot_data_t *key_slot_data,\n\t\t\t\t\t uint32_t slot_id)\n{\n\tint kmu_status;\n\tuint8_t orig_write_buf_size;\n\tcracen_kmu_key_slot_provision_write_enable_set(true, &orig_write_buf_size);\n\tkmu_status = nrfx_kmu_key_slot_provision(key_slot_data, slot_id);\n\tcracen_kmu_key_slot_provision_write_enable_set(false, &orig_write_buf_size);\n\treturn kmu_status;\n}\n```\n```\nint nrfx_kmu_key_slot_provision(nrfx_kmu_key_slot_data_t const * p_key_slot_data, uint32_t slot_id)\n{\n    NRFX_ASSERT((m_cb.state == NRFX_DRV_STATE_INITIALIZED) &&\n                (p_key_slot_data) &&\n                (slot_id < KMU_KEYSLOTNUM));\n    bool is_ready = false;\n    NRFX_WAIT_FOR(nrf_kmu_status_get(NRF_KMU) == 0, 500, 10, is_ready);\n    if (!is_ready)\n    {\n        return -EAGAIN;\n    }\n    nrf_kmu_src_set(NRF_KMU, (uint32_t)p_key_slot_data);\n    nrf_kmu_keyslot_set(NRF_KMU, slot_id);\n    nrf_kmu_task_trigger(NRF_KMU, NRF_KMU_TASK_PROVISION_KEYSLOT);\n    return wait_for_task_result(NRF_KMU_EVENT_EVENTS_PROVISIONED);\n}\n```\nThe push operation is significantly simpler, only requiring that the desired\nslot be configured in the `KEY_SLOT`\nregister,\nand the task be triggered by a write to the `TASKS_PUSH`\nregister\n(`0x50049004`).\n\n```\n(gdb) set *(unsigned int*)(0x50049500) = 9\n```\n```\n(gdb) set *(unsigned int*)(0x50049004) = 1\n```\nSimilarly to the provision operation, I then checked the `EVENTS_PUSHED`\nregister\n(`0x50049104`) to ensure the operation was successful.\n\n```\n(gdb) x/1wx 0x50049104\n```\n```\n0x50049104:\t0x00000001\n```\nWith the `EVENTS_PUSHED` register indicating a successful operation, I finally\nchecked the destination address (`0x20000000`) that I had specified in the `SRC`\nstruct, which I expected to now hold the value.\n\n```\n(gdb) x/4wx 0x20000000\n```\n```\n0x20000000:\t0xab23c1ab\t0xc1ab23c1\t0x23c1ab23\t0xab23c1ab\n```\nPleased that I had successfully completed the operation, I attempted to repeat\nthe sequence of steps, this time using key slot `10` instead of `9`, and 16\nbytes of a `def456` sequence instead of `abc123` as the value. I issued the same\n4 word read on `0x20000000` because I had reused the same destination address in\nslot `10` as I had in slot `9`. To my surprise the contents still matched the\nprevious value.\n\n```\n(gdb) x/4wx 0x20000000\n```\n```\n0x20000000:\t0xab23c1ab\t0xc1ab23c1\t0x23c1ab23\t0xab23c1ab\n```\nThis seemed rather peculiar, and my initial assumption was that I must have\nmissed a step when repeating the operation. However, no matter how many times I\nattempted to push to same address (`0x20000000`), the value remained the same.\nAfter erasing the device, I observed that the first push would correctly update\nthe value at the destination address, while subsequent pushes would not.\n\nWhile astute readers may already be smelling a stale cache, it is worth taking a step back and examining the debug architecture of the nRF54LM20. Like most Arm systems, it implements to the Arm Debug Interface (ADI), specifically leveraging the Arm CoreSight SoC-400 implementation. It has three access ports: two standard AHB-AP and one custom CTRL-AP. The first AHB-AP is used to communicate with the main Cortex-M33 CPU, while the latter is used for accessing auxiliary units, specifically the RISC-V VPR coprocessor. The aforementioned CTRL-AP enables a small subset of functionality that is typically leveraged in a scenario in which the AHB-APs have been disabled (i.e. access port protection is enabled).\n\nIn order for GDB to interact with the J-Link OB, it needs something to translate\nbetween the commands it supports and those supported by the underlying debugger.\n`JLinkGDBServer`\nplays that role when working with J-Link debuggers. GDB effectively acts as a\nconsistent interface to heterogeneous backends, so when you want to read the\ncontents of a given memory address, you can use the same command whether you are\ndebugging a microcontroller or a process on your local development machine.\n\nYou can also issue commands directly to the GDB server implementation using the\nthe GDB `monitor` command. For example, `JLinkGDBServer` supports a `ReadMemAP`\ncommand, which allows you to\ndirectly specify the access port to target, the memory address, the number of\nitems, and a set of flags. Suspecting that my push operations may be succeeding,\nbut my reads returning stale values, I issued a `ReadMemAP` command with the\nsame parameters as my GDB memory read commands.\n\n```\n(gdb) monitor ReadMemAP 0x0 0x20000000 4 0\n```\n```\nO.K.:0xDE56F4DE,0xF4DE56F4,0x56F4DE56,0xDE56F4DE\n```\nSure enough, reading directly from the AHB-AP showed the expected value.\nFurthermore, after issuing the read, subsequent examine (`x`) commands from GDB\ncontinued to return stale values. GDB and `JLinkGDBServer` communicate using the\nGDB Remote Serial Protocol\n(RSP),\nand the specific packets transmitted between them can be observed by enabling\nremote debug logging.\n\n```\n(gdb) set debug remote 1\n```\nGiven the observed behavior, I suspected that the two different memory read strategies used different RSP packets. This was confirmed after issuing commands with the debug logging enabled.\n\n```\n(gdb) x/4wx 0x20000000\n[remote] Packet received: b??}\\003?\n0xab23c1ab\t[remote] Sending packet: $x20000004,4#5e\n[remote] Packet received: b?}\\003??\n0xc1ab23c1\t[remote] Sending packet: $x20000008,4#62\n[remote] Packet received: b}\\003??}\\003\n0x23c1ab23\t[remote] Sending packet: $x2000000c,4#8d\n[remote] Packet received: b??}\\003?\n0xab23c1ab\n```\n```\n(gdb) monitor ReadMemAP 0x0 0x20000000 4 0\n[remote] Sending packet: $qRcmd,526561644d656d415020307830203078323030303030303020342030#a0\n[remote] Packet received: 4f2e4b2e3a307844453536463444452c307846344445353646342c307835364634444535362c307844453536463444450D0A\nO.K.:0xDE56F4DE,0xF4DE56F4,0x56F4DE56,0xDE56F4DE\n```\nIn fact, the `ReadMemAP` command is passed hex encoded directly to\n`JLinkGDBServer` using a `qRcmd` (remote command query) packet.\n\n```\necho 526561644d656d415020307830203078323030303030303020312030 | xxd -r -p\n```\n```\nReadMemAP 0x0 0x20000000 4 0\n```\nThe question of why `JLinkGDBServer` opted to return stale values for one read\nand not the other remained. Though the documentation on `JLinkArm.dll`, the\nunderlying library that most J-Link tooling depends on, is fairly light, there\nis a list of supported command\nstrings that gives a clue as to\nits internal caching behavior. Specifically, the `SetEnableMemCache`\ncommand is\ndefined as controlling memory caching mechanisms, and is on by default. There is\neven a somewhat ominous note about turning it off.\n\nThis command may not be used by any IDE, listed as a supported IDE, to disable memory cache mechanisms by default. It may only be used by specific customers for very specific test cases that needs the cache mechanisms to be disabled.\n\n\nEager to observe if disabling the memory cache actually resulted in fresh values being returned when issuing examine commands, I once again erased the device and connected GDB. Before performing any operations, I disabled the memory cache.\n\n```\n(gdb) monitor exec SetEnableMemCache = 0\n```\nRunning through the provision and push operations for the first slot, I observed the expected value as before. Then on the second provision and push, the examine command finally returned the updated value.\n\n```\n(gdb) x/4wx 0x20000000\n```\n```\n0x20000000:\t0xde56f4de\t0xf4de56f4\t0x56f4de56\t0xde56f4de\n```\nHowever, as the J-Link documentations states, you typically do not want to turn\noff caching. The reason why the `JLinkArm.dll` memory cache returns stale values\nin this case is because the CPU is halted and we are attempting to read from a\nmemory address that we have already accessed without advancing the CPU. With the\nmemory cache enabled, advancing the CPU a few instructions (`stepi`) results in\nthe cache being cleared and a fresh value being returned on the next read.\n\nOutside of use cases where a peripheral, such as the nRF54LM20’s KMU, has direct memory access (DMA) and can write while the core is halted, you typically won’t encounter issues with stale debugger memory cache values. In the event that you do, it can be helpful to understand the underlying bus architecture and how to bypass the cache by reading directly from an access port.","body_html":"<p>I’ve spent the last few weeks working with the security architecture of the nRF54L series from Nordic Semiconductor (in case you missed it, I recently joined Nordic!). While doing so, I have engaged my typical low-level learning technique of eschewing writing firmware for manually poking at registers using the debugger. A few nights ago I found myself observing unexpected values in memory when working with the key management unit. It turned out to be a familiar issue, but one that requires an understanding of the internal system on chip (SoC) components, and how the debugger interacts with them, to diagnose.</p>\n<p>For a bit of background, the nRF54L series has a fairly advanced set of security capabilities, headlined by Arm TrustZone support in the Cortex-M33 core, a CRACEN cryptographic accelerator, and a Key Management Unit (KMU). The KMU is used for storing sensitive data, such as key seeds and associated metadata, in the Secure Information Configuration Region (SICR).</p>\n<p>The SICR is divided into slots. These slots are targeted by issuing tasks to the\nKMU, which can only be accessed in secure mode. Typically, application firmware\ndoesn’t interact with the KMU directly. Instead, PSA\ndrivers are implemented\nto abstract the generation, storage, and usage of keys. For example, if\ninvoking\n<code>psa_generate_key()</code>,\nthe operation eventually results in a call to\n<code>import_key_for_kmu()</code>\nin the CRACEN PSA\ndriver.</p>\n<pre><code>static psa_status_t import_key_for_kmu(const psa_key_attributes_t *attributes, const uint8_t *data,\n                       size_t data_length, uint8_t *key_buffer,\n                       size_t key_buffer_size, size_t *key_buffer_length,\n                       size_t *key_bits)\n{\n    size_t opaque_key_size;\n    psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED;\n    int slot_id =\n        CRACEN_PSA_GET_KMU_SLOT(MBEDTLS_SVC_KEY_ID_GET_KEY_ID(psa_get_key_id(attributes)));\n    psa_key_attributes_t stored_attributes;\n    status = cracen_get_opaque_size(attributes, &amp;opaque_key_size);\n    if (status != PSA_SUCCESS) {\n        return status;\n    }\n    if (key_buffer_size &lt; opaque_key_size) {\n        return PSA_ERROR_BUFFER_TOO_SMALL;\n    }\n    status = cracen_kmu_provision(attributes, slot_id, data, data_length);\n    if (status != PSA_SUCCESS) {\n        return status;\n    }\n    status = cracen_kmu_get_builtin_key(slot_id, &amp;stored_attributes, key_buffer,\n                        key_buffer_size, key_buffer_length);\n    if (status != PSA_SUCCESS) {\n        return status;\n    }\n    *key_bits = psa_get_key_bits(&amp;stored_attributes);\n    return status;\n}</code></pre>\n<p>A slot can either be erased, provisioned, or revoked. The datasheet includes a helpful diagram of the state machine.</p>\n<p>Slots have an ID (<code>0</code> - <code>249</code>) and can store metadata (32 bits), a destination\naddress (32 bits), a value (128 bits), and a revocation policy (2 bits). The\nlatter determines how state changes when a key is in the provisioned state and\nvarious tasks that reference its slot ID are issued to the KMU. When\nexperimenting with the KMU, it is easiest to use the <code>ROTATING</code> revocation\npolicy, which dictates that the slot transitions back to the erased state when a\nrevoke task is issued.</p>\n<p>When the <code>PUSH</code> task is issued, the value in the slot is written to the\ndestination address that was specified when the key was provisioned. The\nprovisioning process is documented in the\ndatasheet,\nbut it can also be seen in the <code>cracen_kmu_key_slot_provision()</code>\nimplementation:</p>\n<pre><code>static int cracen_kmu_key_slot_provision(const nrfx_kmu_key_slot_data_t *key_slot_data,\n                     uint32_t slot_id)\n{\n    int kmu_status;\n    uint8_t orig_write_buf_size;\n    cracen_kmu_key_slot_provision_write_enable_set(true, &amp;orig_write_buf_size);\n    kmu_status = nrfx_kmu_key_slot_provision(key_slot_data, slot_id);\n    cracen_kmu_key_slot_provision_write_enable_set(false, &amp;orig_write_buf_size);\n    return kmu_status;\n}</code></pre>\n<p>The <code>nrfx_kmu_key_slot_data_t</code> definition can be found in the Nordic Zephyr\nHardware Abstraction Layer\n(HAL).</p>\n<pre><code>typedef struct __PACKED\n{\n    uint32_t                     keyslot_value[KEY_SLOT_WORDS_COUNT]; ///&lt; Key data to be provisioned.\n#if NRF_KMU_HAS_REVOKE_POLICY || defined(__NRFX_DOXYGEN__)\n    uint32_t                     revoke_policy;                       /**&lt; Key revoke policy.\n                                                                       *   @ref nrfx_kmu_rpolicy_t\n                                                                       *   holds possible values. */\n#endif\n    uint32_t                     keyslot_dest;                        /**&lt; Key slot destination when\n                                                                       *   performing key push. */\n#if NRF_KMU_HAS_METADATA || defined(__NRFX_DOXYGEN__)\n    nrfx_kmu_key_slot_metadata_t metadata;                            ///&lt; Metadata to write to keyslot.\n#endif\n} nrfx_kmu_key_slot_data_t;</code></pre>\n<p>You could write some fairly straightforward firmware, or even use the CRACEN KMU sample, build it, then flash it onto a development kit to easily provision a key to the KMU. However, if using the supported drivers (which you absolutely should), additional restrictions are placed on the values that you can use when provisioning a key slot. For example, while the KMU supports any 32 bit value for metadata, the PSA driver assigns meaning to each of the bits.</p>\n<pre><code>typedef struct kmu_metadata {\n    uint32_t metadata_version: 4;\n    uint32_t key_usage_scheme: 2;\n    uint32_t reserved: 8;\n    uint32_t algorithm: 6;\n    uint32_t size: 3;\n    uint32_t rpolicy: 2;\n    uint32_t usage_flags: 7;\n} kmu_metadata;</code></pre>\n<p>Similarly, there are restrictions on the values that you can write and the destination to which a given type of key is pushed. If manually interacting with the KMU, the metadata, value, and destination are much more flexible. However, if you provision non-conformant data into slots in the KMU, then attempt to interact with it using the supported drivers, you are going to have a bad time.</p>\n<p>Knowing the risks, and that I could restore the SICR on nRF54LM20\nDK with\nan\n<code>ERASEALL</code>\noperation on the control access port\n(CTRL-AP),\nI had powered up the board and connected with\nGDB. As previously mentioned, the\nKMU can only be accessed in secure mode. However, when access port protection\nis not\nenabled,\nthe Secure Privileged Invasive Debug Enable\n(SPIDEN)\nsignal is driven high, and the on-board J-Link debugger (J-Link\nOB) can\noperate with secure privileges.</p>\n<p>With the CPU halted, I tested that I was able to access the KMU and determined\nthat it was ready for operations by reading from the <code>STATUS</code>\nregister\n(<code>0x50049400</code>).</p>\n<pre><code>(gdb) x/1xw 0x50049400</code></pre>\n<pre><code>0x50049400:    0x00000000</code></pre>\n<p>To test the actual functionality, I followed the\nprovisioning\nand\npush\nsteps described in the datasheet. The first step was to build the <code>SRC</code> data\nstructure in memory, which is of the format specified in the packed\n<code>nrfx_kmu_key_slot_data_t</code> struct definition. To make testing multiple values\nsimpler, I wrote a tiny Python script to build the struct.</p>\n<pre><code>import struct\nopen(&quot;kmu_src.bin&quot;, &quot;wb&quot;).write(\n    bytes.fromhex(&quot;abc123abc123abc123abc123abc123ab&quot;)  # Value\n    + struct.pack(\n        &quot;&lt;III&quot;,\n        3,  # Revocation Policy\n        0x20000000,  # Destination Address\n        0xDEF678DE,  # Metadata\n    )\n)</code></pre>\n<p>The produced <code>kmu_src.bin</code> contained the following contents.</p>\n<pre><code>xxd kmu_src.bin</code></pre>\n<pre><code>00000000: abc1 23ab c123 abc1 23ab c123 abc1 23ab  ..#..#..#..#..#.\n00000010: 0300 0000 0000 0020 de78 f6de            ....... .x..</code></pre>\n<p>To write the data to memory, I used the GDB <code>restore</code> command.</p>\n<pre><code>(gdb) restore kmu_src.bin binary 0x20001000</code></pre>\n<p>The next step was to write the address of the struct (<code>0x20001000</code>) to the KMU\n<code>SRC</code>\nregister\n(<code>0x50049504</code>) and specify the desired key slot (<code>9</code>) in the <code>KEY_SLOT</code>\nregister\n(<code>0x50049500</code>).</p>\n<pre><code>(gdb) set *(unsigned int*)(0x50049504) = 0x20001000</code></pre>\n<pre><code>(gdb) set *(unsigned int*)(0x50049500) = 9</code></pre>\n<p>Before actually issuing the task, the resistive random access memory controller\n(RRAMC)\nmust be configured to allow unbuffered writes. I stored the current RRAMC\n<code>CONFIG</code>\nregister\n(<code>0x5004e500</code>) in a variable to be restored after completion of the task, then\nwrote the value <code>1</code>, which sets write enable (<code>WEN</code>) field to <code>1</code> and the buffer\nsize (<code>WRITEBUFSIZE</code>) to <code>0</code> (unbuffered).</p>\n<pre><code>(gdb) set $rramc_config = *(unsigned int*)(0x5004e500)\n(gdb) set *(unsigned int*)(0x5004e500) = 1</code></pre>\n<p>Finally, I wrote <code>1</code> to the <code>TASKS_PROVISION</code>\nregister\n(<code>0x50049000</code>), instructing the KMU to store the value and its metadata in key\nslot <code>9</code>. I verified the event was generated by subsequently reading the\n<code>EVENTS_PROVISIONED</code>\nregister\n(<code>0x50049100</code>).</p>\n<pre><code>(gdb) set *(unsigned int*)(0x50049000) = 1</code></pre>\n<pre><code>(gdb) x/1wx 0x50049100</code></pre>\n<pre><code>0x50049100:    0x00000001</code></pre>\n<p>With the task completed, I then reset the RRAMC <code>CONFIG</code> register.</p>\n<pre><code>(gdb) set *(unsigned int*)(0x5004e500) = $rramc_config</code></pre>\n<p>These exact operations can also be seen in the CRACEN PSA\ndriver’s\n<code>cracen_kmu_key_slot_provision()</code> and the underlying\n<code>nrfx_kmu_key_slot_provision()</code>\nfunction.</p>\n<pre><code>static int cracen_kmu_key_slot_provision(const nrfx_kmu_key_slot_data_t *key_slot_data,\n                     uint32_t slot_id)\n{\n    int kmu_status;\n    uint8_t orig_write_buf_size;\n    cracen_kmu_key_slot_provision_write_enable_set(true, &amp;orig_write_buf_size);\n    kmu_status = nrfx_kmu_key_slot_provision(key_slot_data, slot_id);\n    cracen_kmu_key_slot_provision_write_enable_set(false, &amp;orig_write_buf_size);\n    return kmu_status;\n}</code></pre>\n<pre><code>int nrfx_kmu_key_slot_provision(nrfx_kmu_key_slot_data_t const * p_key_slot_data, uint32_t slot_id)\n{\n    NRFX_ASSERT((m_cb.state == NRFX_DRV_STATE_INITIALIZED) &amp;&amp;\n                (p_key_slot_data) &amp;&amp;\n                (slot_id &lt; KMU_KEYSLOTNUM));\n    bool is_ready = false;\n    NRFX_WAIT_FOR(nrf_kmu_status_get(NRF_KMU) == 0, 500, 10, is_ready);\n    if (!is_ready)\n    {\n        return -EAGAIN;\n    }\n    nrf_kmu_src_set(NRF_KMU, (uint32_t)p_key_slot_data);\n    nrf_kmu_keyslot_set(NRF_KMU, slot_id);\n    nrf_kmu_task_trigger(NRF_KMU, NRF_KMU_TASK_PROVISION_KEYSLOT);\n    return wait_for_task_result(NRF_KMU_EVENT_EVENTS_PROVISIONED);\n}</code></pre>\n<p>The push operation is significantly simpler, only requiring that the desired\nslot be configured in the <code>KEY_SLOT</code>\nregister,\nand the task be triggered by a write to the <code>TASKS_PUSH</code>\nregister\n(<code>0x50049004</code>).</p>\n<pre><code>(gdb) set *(unsigned int*)(0x50049500) = 9</code></pre>\n<pre><code>(gdb) set *(unsigned int*)(0x50049004) = 1</code></pre>\n<p>Similarly to the provision operation, I then checked the <code>EVENTS_PUSHED</code>\nregister\n(<code>0x50049104</code>) to ensure the operation was successful.</p>\n<pre><code>(gdb) x/1wx 0x50049104</code></pre>\n<pre><code>0x50049104:    0x00000001</code></pre>\n<p>With the <code>EVENTS_PUSHED</code> register indicating a successful operation, I finally\nchecked the destination address (<code>0x20000000</code>) that I had specified in the <code>SRC</code>\nstruct, which I expected to now hold the value.</p>\n<pre><code>(gdb) x/4wx 0x20000000</code></pre>\n<pre><code>0x20000000:    0xab23c1ab    0xc1ab23c1    0x23c1ab23    0xab23c1ab</code></pre>\n<p>Pleased that I had successfully completed the operation, I attempted to repeat\nthe sequence of steps, this time using key slot <code>10</code> instead of <code>9</code>, and 16\nbytes of a <code>def456</code> sequence instead of <code>abc123</code> as the value. I issued the same\n4 word read on <code>0x20000000</code> because I had reused the same destination address in\nslot <code>10</code> as I had in slot <code>9</code>. To my surprise the contents still matched the\nprevious value.</p>\n<pre><code>(gdb) x/4wx 0x20000000</code></pre>\n<pre><code>0x20000000:    0xab23c1ab    0xc1ab23c1    0x23c1ab23    0xab23c1ab</code></pre>\n<p>This seemed rather peculiar, and my initial assumption was that I must have\nmissed a step when repeating the operation. However, no matter how many times I\nattempted to push to same address (<code>0x20000000</code>), the value remained the same.\nAfter erasing the device, I observed that the first push would correctly update\nthe value at the destination address, while subsequent pushes would not.</p>\n<p>While astute readers may already be smelling a stale cache, it is worth taking a step back and examining the debug architecture of the nRF54LM20. Like most Arm systems, it implements to the Arm Debug Interface (ADI), specifically leveraging the Arm CoreSight SoC-400 implementation. It has three access ports: two standard AHB-AP and one custom CTRL-AP. The first AHB-AP is used to communicate with the main Cortex-M33 CPU, while the latter is used for accessing auxiliary units, specifically the RISC-V VPR coprocessor. The aforementioned CTRL-AP enables a small subset of functionality that is typically leveraged in a scenario in which the AHB-APs have been disabled (i.e. access port protection is enabled).</p>\n<p>In order for GDB to interact with the J-Link OB, it needs something to translate\nbetween the commands it supports and those supported by the underlying debugger.\n<code>JLinkGDBServer</code>\nplays that role when working with J-Link debuggers. GDB effectively acts as a\nconsistent interface to heterogeneous backends, so when you want to read the\ncontents of a given memory address, you can use the same command whether you are\ndebugging a microcontroller or a process on your local development machine.</p>\n<p>You can also issue commands directly to the GDB server implementation using the\nthe GDB <code>monitor</code> command. For example, <code>JLinkGDBServer</code> supports a <code>ReadMemAP</code>\ncommand, which allows you to\ndirectly specify the access port to target, the memory address, the number of\nitems, and a set of flags. Suspecting that my push operations may be succeeding,\nbut my reads returning stale values, I issued a <code>ReadMemAP</code> command with the\nsame parameters as my GDB memory read commands.</p>\n<pre><code>(gdb) monitor ReadMemAP 0x0 0x20000000 4 0</code></pre>\n<pre><code>O.K.:0xDE56F4DE,0xF4DE56F4,0x56F4DE56,0xDE56F4DE</code></pre>\n<p>Sure enough, reading directly from the AHB-AP showed the expected value.\nFurthermore, after issuing the read, subsequent examine (<code>x</code>) commands from GDB\ncontinued to return stale values. GDB and <code>JLinkGDBServer</code> communicate using the\nGDB Remote Serial Protocol\n(RSP),\nand the specific packets transmitted between them can be observed by enabling\nremote debug logging.</p>\n<pre><code>(gdb) set debug remote 1</code></pre>\n<p>Given the observed behavior, I suspected that the two different memory read strategies used different RSP packets. This was confirmed after issuing commands with the debug logging enabled.</p>\n<pre><code>(gdb) x/4wx 0x20000000\n[remote] Packet received: b??}\\003?\n0xab23c1ab    [remote] Sending packet: $x20000004,4#5e\n[remote] Packet received: b?}\\003??\n0xc1ab23c1    [remote] Sending packet: $x20000008,4#62\n[remote] Packet received: b}\\003??}\\003\n0x23c1ab23    [remote] Sending packet: $x2000000c,4#8d\n[remote] Packet received: b??}\\003?\n0xab23c1ab</code></pre>\n<pre><code>(gdb) monitor ReadMemAP 0x0 0x20000000 4 0\n[remote] Sending packet: $qRcmd,526561644d656d415020307830203078323030303030303020342030#a0\n[remote] Packet received: 4f2e4b2e3a307844453536463444452c307846344445353646342c307835364634444535362c307844453536463444450D0A\nO.K.:0xDE56F4DE,0xF4DE56F4,0x56F4DE56,0xDE56F4DE</code></pre>\n<p>In fact, the <code>ReadMemAP</code> command is passed hex encoded directly to\n<code>JLinkGDBServer</code> using a <code>qRcmd</code> (remote command query) packet.</p>\n<pre><code>echo 526561644d656d415020307830203078323030303030303020312030 | xxd -r -p</code></pre>\n<pre><code>ReadMemAP 0x0 0x20000000 4 0</code></pre>\n<p>The question of why <code>JLinkGDBServer</code> opted to return stale values for one read\nand not the other remained. Though the documentation on <code>JLinkArm.dll</code>, the\nunderlying library that most J-Link tooling depends on, is fairly light, there\nis a list of supported command\nstrings that gives a clue as to\nits internal caching behavior. Specifically, the <code>SetEnableMemCache</code>\ncommand is\ndefined as controlling memory caching mechanisms, and is on by default. There is\neven a somewhat ominous note about turning it off.</p>\n<p>This command may not be used by any IDE, listed as a supported IDE, to disable memory cache mechanisms by default. It may only be used by specific customers for very specific test cases that needs the cache mechanisms to be disabled.</p>\n<p>Eager to observe if disabling the memory cache actually resulted in fresh values being returned when issuing examine commands, I once again erased the device and connected GDB. Before performing any operations, I disabled the memory cache.</p>\n<pre><code>(gdb) monitor exec SetEnableMemCache = 0</code></pre>\n<p>Running through the provision and push operations for the first slot, I observed the expected value as before. Then on the second provision and push, the examine command finally returned the updated value.</p>\n<pre><code>(gdb) x/4wx 0x20000000</code></pre>\n<pre><code>0x20000000:    0xde56f4de    0xf4de56f4    0x56f4de56    0xde56f4de</code></pre>\n<p>However, as the J-Link documentations states, you typically do not want to turn\noff caching. The reason why the <code>JLinkArm.dll</code> memory cache returns stale values\nin this case is because the CPU is halted and we are attempting to read from a\nmemory address that we have already accessed without advancing the CPU. With the\nmemory cache enabled, advancing the CPU a few instructions (<code>stepi</code>) results in\nthe cache being cleared and a fresh value being returned on the next read.</p>\n<p>Outside of use cases where a peripheral, such as the nRF54LM20’s KMU, has direct memory access (DMA) and can write while the core is halted, you typically won’t encounter issues with stale debugger memory cache values. In the event that you do, it can be helpful to understand the underlying bus architecture and how to bypass the cache by reading directly from an access port.</p>","headings":[]}}