Talos Vulnerability Report

TALOS-2025-2289

Tp-Link AX53 v1.0 tmpServer opcode 0x1003 stack-based buffer overflow vulnerability

March 16, 2026
CVE Number

CVE-2025-58455

SUMMARY

A stack-based buffer overflow vulnerability exists in the tmpServer opcode 0x1003 functionality of Tp-Link AX53 v1.0 1.3.1 Build 20241120 rel.54901(5553). A specially crafted network packets can lead to arbitrary code execution. An attacker can send packets to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Tp-Link Archer AX53 v1.0 1.3.1 Build 20241120 rel.54901(5553)

PRODUCT URLS

Archer AX53 v1.0 - https://www.tp-link.com/my/support/download/archer-ax53/

CVSSv3 SCORE

9.1 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-121 - Stack-based Buffer Overflow

DETAILS

The TP-Link Archer AX53 AX3000 Dual Band Gigabit Wi-Fi 6 Router is currently among the most popular routers sold online, and boasts impressive gigabit speeds for the price. This router also features remote cloud access via the TP-Link HomeShield application and smart home functionality.

In order to facilitate remote management of the TP-Link AX53 and many other TP-Link devices, the HomeShield phone app can connect to the device through the cloud. After authentication, an SSH port forward is setup from the cloud to the TP-Link router, which then allows for communication to network services listening on the TP-Link router’s localhost interfaces. Specifically for the HomeShield app, this is the tmpServer network service, which listens on TCP 127.0.0.1:20002. This service is able to modify a large amount of settings and configuration on the TP-Link router, however it is not 1-1 to the admin webportal that is accessible via LAN. Regardless, we start from where the tmpServer service starts parsing network packets:

000247b4    int32_t p2_recv(struct tmp_client* tmpcli)
// [...]
00024808        int32_t bytes_recvd =
00024808            recv(__fd: tmpcli->fd, __buf: get_read_buf_ptr(tmpcli), __n: get_bytes_left_in_buf(tmpcli), __flags: 0)  // [1]
00024808        
00024818        if (bytes_recvd s< 0)
0002482c            log("tmpdServer.c:1382", "TMP RECV ERROR")
00024830            return 0xffffffff
00024830        
// [...] 
000248dc        tmpcli->insize += bytes_recvd
000248fc        log("tmpdServer.c:1398", "TMP RECV DATA length = %d", tmpcli->insize)
000248fc        
00024910        while (true)  
00024910            if (tmpcli->insize s<= 2)
00024914                return 0
00024914            
00024920            uint32_t authhdrlen = GET_AUTH(tmpcli)            // [2]
00024920            
00024930            if (authhdrlen s< 0)
00024944                log("tmpdServer.c:1413", "GET AUTH ERROR")
00024948                return 0xffffffff
00024948            
00024960            if (tmpcli->insize s<= authhdrlen)
00024964                return 0
00024964            
00024970            int32_t* datasize = GET_PKT_BY_STATUS(tmpcli)   // [3]

As always, our code flow starts from the call to recv[1], and in this case up to 0x4000 bytes can be sent in a single message or set of messages and then read in without error. The check at [2] doesn’t really do much important if our opcode is not \xfe, which it never is, and then at [3] some actual checks are made depending on the state of the client connection. A quick digression into the client connection structure before continuing into the code:

struct tmp_client __packed
{
    uint32_t fd;
    void* sysinfo;
    uint32_t status;
    struct TmpPktIn* tdpin;
    uint32_t insize;
    struct TdpPkt* out;
    uint32_t outpkt_size;  
    uint32_t serviceType;
    uint32_t serviceMask;
    uint32_t authHdrLen;
};

Most of these fields should be self explanatory, but it’s worth noting that the status field always starts at 0x1 for new connections, the tdpin buffer is max size 0x4000, and the tdpout buffer is also max size 0x4000. Continuing in the code, before we can send any actual data, we must perform a mini handshake in the form of ASSOCIATION packets, which we can see continuing into GET_PKT_BY_STATUS[3]:

000245e8    int32_t GET_PKT_BY_STATUS(struct tmp_client* tmpcli)

000245fc        uint32_t status = tmpcli->status
000245fc        
00024604        if (status s>= 1)
0002460c            if (status s<= 2)
0002462c                log("tmpdServer.c:1264", "TMP GET ASSOC PKT FROM BUF")
00024638                return check_assoc_pktlen_at_least_4(tmpcli) 
00024638            
00024614            if (status == 3)
00024650                log("tmpdServer.c:1268", "TMP GET DATA PKT FROM BUF")
0002465c                return get_data_pkt_from_buf(tmpcli)
0002465c        
00024664        return 0xffffffff

As mentioned, our client connection always starts at 0x1, so we hit the function at 0x24638, which just checks to see if the size of the input packet minus the size of the auth header (which can be up to 0x40) is greater than 0x4. Assuming this is true, we step back up to the function that started with recv:

//[...]
00024970            int32_t* datasize = GET_PKT_BY_STATUS(tmpcli)   // [3]
00024980            if (datasize s< 0)
00024994                log("tmpdServer.c:1427", "GET PKT ERROR")
00024998                return 0xffffffff
00024998            
000249a8            if (datasize == 0)
000249a8                break
000249a8            
000249d0            int32_t result = ENTER_ASSOC_OR_DATA_CENTER(tmpcli, datasize) //[4]
000249d0            

Immediately after hopefully passing the initial checks, we enter the function at [4] which processes both ASSOC packets and DATA packets:

00023d10    int32_t ENTER_ASSOC_OR_DATA_CENTER(struct tmp_client* tmpcli, int32_t* datasize)

00023d28        uint32_t status = tmpcli->status
00023d28        
00023d30        if (status s>= 1)
00023d38            if (status s<= 2)
00023d58                log("tmpdServer.c:1087", "ENTER ASSOC CENTER")
00023d64                return assoc_parse_set_tmpcli_status(tmpcli)  // [5]
00023d64            
00023d40            if (status == 3)
00023d8c                return decode_data_pkt(tmpcli, datasize, log("tmpdServer.c:1091", "ENTER DATA CENTER")) // [6]
00023d8c        
00023d94        return 0

Since our status is still currently 0x1, we enter the function at [5]:

00023610    int32_t assoc_parse_set_tmpcli_status(struct tmp_client* tmpcli)

00023624        int32_t var_18 = 0
0002362c        int32_t var_1c = 0
00023634        int32_t var_20 = 0
0002363c        uint32_t authHdrLen = tmpcli->authHdrLen
0002363c        
0002364c        if (tmpcli == 0)
00023650            return 0xffffffff
00023650        
00023674        memset(s: tmpcli->out, c: 0, n: 0x4000)
0002367c        struct TdpPkt* out = tmpcli->out
00023694        out->rbuf_start_ver = 1
000236a8        out->reserved_0x0_or_0xf0 = 0
000236b4        out->opcode:1.b = 0
000236c4        struct assoc_pkt* assoc = tmpcli->tdpin + authHdrLen
000236c4        
000236d8        if (tmpcli->status == 1)
000236e8            if (zx.d(assoc->assoc_opcode) == 1 && is_zero(assoc->is_a_request) != 0)
00023718                log("tmpdServer.c:897", "GET TMP ASSOC REQUEST PKT")
00023724                out->opcode.b = TDP_REPEATER_SG_0x2
00023754                int32_t result = send(__fd: tmpcli->fd, __buf: tmpcli->out, __n: 4, __flags: 0)
00023754                
00023768                if (4 != result)
00023770                    result = 0xffffffff
00023770                
0002377c                set_tmpcli_status(tmpcli, status: 2)
000238ac                return result

Without much detail, for ASSOC packets, there’s only rudimentary checks, and a packet containing just \x00\x00\x01\x00 will allow us to get past the first step. This sets our status to 0x2, and if we send another packet, we will hit a different code branch within assoc_parse_set_tmpcli_status:

000237fc        if (zx.d(assoc->assoc_opcode) == 2 && is_zero(assoc->is_a_request) != 0
000237fc                && is_one_and_zero(assoc->needs_1, assoc->needs_0) != 0)
00023854            log("tmpdServer.c:920", "GET TMP ASSOC ACCEPT PKT")
00023860            set_tmpcli_status(tmpcli, status: 3)
00023868            return 0

Likewise for a status of 0x2, the checks are still rudimentary and a packet of \x01\x00\x02\x00 allows us to reach the actual data packet processing within decode_data_pkt(tmpcli, datasize, log("tmpdServer.c:1091", "ENTER DATA CENTER") which requires us to have a status of 0x3 [6]:

000238bc    int32_t decode_data_pkt(struct tmp_client* tmpcli, uint32_t datasize, int32_t arg3)

000238ec        uint32_t authHdrLen = tmpcli->authHdrLen
000238f8        int32_t out_function_cb = 0
000238fc        void* tmp = tmpcli
000238fc        
00023904        if (tmp != 0)
0002390c            tmp = tmpcli->status
0002390c            
00023914            if (tmp == 3)
0002391c                arg3 = 0x4000
00023920                tmp = datasize
00023920                
00023928                if (0x4000 s>= tmp)
00023954                    struct TmpDataPkt* in = tmpcli->tdpin + authHdrLen
00023974                    int32_t decode_ret = check_tdp_data_in(in: tmpcli->tdpin + authHdrLen, inpsize: datasize) // [7]

Starting out, assuming we’re within the 0x4000 size limit, we hit the first check on our data packet at [7]. Since the format for DATA packets is not the same as ASSOC packets, an example DATA HELLO packet is given below:

pkt3 =  b""
#pkt3 += b"\x00" # opcode. \xfe => needs auth len/auth header
#pkt3 += b"\x00" # serNameLen  
#pkt3 += b"\x00" # service mask
# auth stuff would go here
# need at least 0x10 bytes for datapkt
pkt3 += b"\x01"             # Need 0x1
pkt3 += b"\x00"             # Need 0x0
pkt3 += b"\x04"             # opcode   # opcode != 0x5 => len 0x0 
pkt3 += b"\x00"             # idk 0x3 
pkt3 += b"\x00\x00"         # datasize BE, max 0x3ff0 (doesn't include 0x10 hdr) 
pkt3 += b"\x00"             # flags    # opcode != 0x5 => 0x0
pkt3 += b"\x00"             # errcode
pkt3 += b"\x00\x00\x00\x00" # tdp_sn  
pkt3 += b"\x00\x00\x00\x00" # checksum

Continuing into check_tdp_data_in:

00022e0c    int32_t check_tdp_data_in(struct TmpDataPkt* in, int32_t inpsize)
// [...]
00022e34        
00022e68        if (is_one_and_zero(in->need_0x1, in->need_0x0) != 0 && is_zero(in->need_0x0) != 0)              // [8]
00022e9c            uint32_t r0_5 = ntohl(in->checksum)
00022eb8            in->checksum = htonl(0x5a6b7c8d)
00022ec4            int32_t r0_8 = do_checksoum(in, inpsize)
00022ec4            
00022ed8            if (r0_8 != r0_5)                                                                            // [9]
00022ef4                log("tmpdServer.c:681", "TMP curCheckSum=%x; newCheckSum=%x", r0_5, r0_8, inpsize, in)
00022ef8                return 3
00022ef8            
00022f08            in->checksum = r0_5
00022f28            in->datalen = ntohs(in->datalen)
00022f28            
00022f60            if (zx.d(in->opcode) == 5 && zx.d(in->datalen) != 0 && zx.d(in->datalen) + 0x10 != inpsize)
00022f78                log("tmpdServer.c:694", "TMP DATA TRANSFER PKT LENGTH ERROR %d", inpsize, inpsize, inpsize, in)
00022f7c                return 4
00022f7c            
00022fa0            if (zx.d(in->opcode) != 5 && zx.d(in->datalen) != 0)
00022fc0                log("tmpdServer.c:701", "TMP PKT LENGTH ERROR %x", zx.d(in->datalen))
00022fc4                return 4
00022fc4            
00022fe4            if (is_zero_(in->flag) == 0)                                      // [10]
00023004                log("tmpdServer.c:708", "TMP FLAG ERROR %x", zx.d(in->flag))
00023008                return 5
00023008            
00023028            in->tmp_sn = ntohl(in->tmp_sn)
00023048            log("tmpdServer.c:716", "TMP SN = %x", in->tmp_sn)
0002304c            return 0
0002304c        
00022e88        return 1

The main things to be aware of here are that we need 0x1 and 0x0 as our first two bytes to pass the check at [8], we need to have a correct CRC to pass the check at [9], and we also need to have our flag field set to 0x0 to pass the check at [10]. Assuming that’s all good, we return back up to decode_data_pkt:

00023974                    int32_t decode_ret = check_tdp_data_in(in: tmpcli->tdpin + authHdrLen, inpsize: datasize) // [7]                
00023984                    if (decode_ret s< 0)
00023998                        log("tmpdServer.c:974", "TMP DECODE PKT error!!!")
0002399c                        return 0xffffffff
0002399c                    
000239c0                    memset(s: tmpcli->out, c: 0, n: 0x4000)
000239c8                    struct TdpPkt* out = tmpcli->out
000239d8                    int32_t build_an_outpkt_flag
000239d8                    
000239d8                    if (decode_ret == 0)
00023a2c                        uint32_t opcode = zx.d(in->opcode)
00023a2c                        
00023a34                        if (opcode == 5)
00023ad0                            log("tmpdServer.c:1003", "TMP RECV DATA PKT")
00023ae8                            tmpcli->sysinfo = get_sysinfo() + 0x3c
00023af8                            out->tdp_sn = in->tmp_sn
00023b1c                            out->errcode = handle_data_recv_(tmpcli, datasize, out_cb: &out_function_cb) & 0xff    // [11]
00023b1c                            
00023b2c                            if (zx.d(out->errcode) != 0)
00023b8c                                log("tmpdServer.c:1018", "TMP DISPATCH PKT ERROR")
00023b98                                out->opcode.b = TDP_IDK_0x6
00023ba4                                out->size_of_data = 0
00023bac                                build_an_outpkt_flag = 0
00023b2c                            else
00023b4c                                log("tmpdServer.c:1011", "TMP DISPATCH PKT OK %d", tmpcli->outpkt_size)
00023b58                                out->opcode.b = TMP_DATA_TRANSFER
00023b6c                                out->size_of_data = (tmpcli->outpkt_size).w & 0xffff
00023b74                                build_an_outpkt_flag = 1
00023a34                        else if (opcode s<= 5)
00023a44                            if (opcode != 4)
00023c20                                log("tmpdServer.c:1044", "TMP RECV ASSOC PKT and CLOSE SOCK")
00023c24                                return 0xffffffff
00023c24                            
00023a70                            log("tmpdServer.c:994", "TMP RECV HELLO PKT")   // [12]
00023a88                            tmpcli->sysinfo = get_sysinfo() + 0x3c
00023a94                            out->opcode.b = TDP_ATTACH_MASTER
00023aa0                            out->size_of_data = 0
00023ab0                            out->tdp_sn = in->tmp_sn
00023ab8                            build_an_outpkt_flag = 1
00023a3c                        else if (opcode == 6)
00023bc4                            log("tmpdServer.c:1026", "TMP RECV BYE PKT")
00023bd0                            out->opcode.b = TDP_IDK_0x6
00023bdc                            out->size_of_data = 0
00023be4                            build_an_outpkt_flag = 0
00023a50                        else
00023a58                            if (opcode != 0xff)
00023c20                                log("tmpdServer.c:1044", "TMP RECV ASSOC PKT and CLOSE SOCK")
00023c24                                return 0xffffffff
00023c24                            
00023bfc                            log("tmpdServer.c:1034", "TMP RECV RENEGOTIATE PKT, SEND BYE PKT TO APP")
00023c00                            send_bye_pkt_to_app()
00023c08                            build_an_outpkt_flag = 0
000239d8                    else
000239f0                        log("tmpdServer.c:983", "TMP PKT error NO. %d", decode_ret, decode_ret, datasize, tmpcli)
000239fc                        out->opcode.b = TDP_IDK_0x6
00023a0c                        out->errcode = decode_ret.b & 0xff
00023a18                        out->size_of_data = 0
00023a20                        build_an_outpkt_flag = 0

Before we can actually send DATA packets with, well, data, we first have to send a dummy DATA HELLO packet with an opcode of 0x5, such that we hit the code path at [12]. After this has happened, we can now send a DATA packet of similar format to hit the branch at [11] and then enter handle_data_recv:

00023e4c    int32_t handle_data_recv_(struct tmp_client* tmpcli, int32_t datasize, void* out_cb)

00023e68        char var_25 = 2
00023e90        uint32_t authHdrLen = tmpcli->authHdrLen
00023e9c        int32_t var_24_1
00023e9c        __builtin_memset(dest: &var_24_1, ch: 0, count: 0x14)
00023e9c        
00023ea8        if (tmpcli == 0)
00023eac            return 0xffffffff
00023eac        
00023ec8        // okay, finally used lol
00023ec8        char serviceType = tmpcli->tdpin->sername[0xd + authHdrLen]
00023ec8        
// [...]
000245a0        
000245a0        while (true) 
000245b8            psess = (&psess1)[x].sesfunc
000245b8            
000245c0            if (psess == 0)
000245c0                break
000245c0            
000242e4            log("tmpdServer.c:1193", "serviceMask is 0x%x, try to login serviceType 0x%x", tmpcli->serviceMask, 
000242e4                zx.d((&psess1)[x].servicetype))
00024308            log("tmpdServer.c:1196", "serviceType is %d,  pSession->serviceType is %d", zx.d(serviceType), tmpcli->serviceType)
0002432c            log("tmpdServer.c:1197", "User Name is %s", &tmpcli->tdpin->sername)
0002432c            
00024370            if (check_service_type_and_mask(servicemask: tmpcli->serviceMask, serviceytpe: (&psess1)[x].servicetype) != 0)
000243d8                if (tmpcli->serviceType == 0 && zx.d((&psess1)[x].servicetype) == zx.d(serviceType))
00024418                    int32_t r0_16 = get_cli_by_ST(ST: serviceType)
0002444c                    int32_t idk = (&psess1)[x].idk
0002444c                    
00024454                    if (r0_16 u>= idk)
0002447c                        log("tmpdServer.c:1216", "TMP_HDR_ERR_BT_EXCEED", &psess1, idk)
00024480                        return 7
00024480                    
00024464                    tmpcli->serviceType = zx.d(serviceType)
00024464                
00024494                // 0x1 or 0xf1
00024494                if (tmpcli->serviceType != 0 && tmpcli->serviceType == zx.d((&psess1)[x].servicetype))
0002452c                    uint32_t r0_20 =  // actually call psess func
0002452c                        (&psess1)[x].sesfunc(&tmpcli->tdpin->sername[0xd + authHdrLen], datasize - 0x10, &tmpcli->out->payload, out_cb)
0002452c                    
0002453c                    if (r0_20 s< 0)
00024550                        log("tmpdServer.c:1231", "TMP_HDR_ERR_BT")
00024554                        return 6
00024554                    
00024564                    tmpcli->outpkt_size = r0_20
00024578                    log("tmpdServer.c:1235", "TMP_HDR_ERR_NONE")
0002457c                    return 0

To summarize the above code without getting too much into detail - based on the uint8_t service_type byte of our input packet, the code will walk a corresponding set of structures to find the opcode that we’re sending and then calling that specific function. An example packet calling the service_type opcode of 0x421 is given below:

#######################################
pkt4 =  b""
pkt4 += b"\x01"             # Need 0x1
pkt4 += b"\x00"             # Need 0x0
pkt4 += b"\x05"             # opcode   # opcode != 0x5 => len 0x0
pkt4 += b"\x00"             # idk 0x3
pkt4 += b"\x00\x00"         # datasize BE, max 0x3ff0 (doesn't include 0x10 hdr). 
pkt4 += b"\x00"             # flags    # opcode != 0x5 => 0x0
pkt4 += b"\x00"             # errcode?
pkt4 += b"\x00\x00\x00\x00" # tdp_sn??
pkt4 += b"\x00\x00\x00\x00" # checksum
# start of psession layer
pkt4 += b"\x01"             # service type (\x01 or \xf1)
pkt4 += b"\x01"             # version
psess_opcode = 0x421
pkt4 += struct.pack(">H",psess_opcode)

   // Begin actual opcode data

We’re almost to the point of talking about specific opcodes, but one more digression must be allowed. Every opcode follows the same overall code flow - our opcode-specific packet data is read in either as a JSON string which is converted to cjson objects or it’s read in via a basic TLV format. This parsed data is potentially written to specific offsets within a size 0x8000 static buffer, with each of the offsets and resulting data formats being opcode specific. This size 0x8000 buffer is then passed to luci wrappers around TP-Link specific lua bytecode binaries which all call their own specific functions. After the lua bytecode binary has finished, the output data is written back into this 0x8000 sized buffer and, assuming no errors have occurred, data is copied to the 0x4000 sized struct TdpPkt* out member of our client session, which is then sent back to us. Unfortunately, this basic overview is extremely important for understanding any of the tmpServer vulnerabilities, so a quick summary of the summary:

[recv()] -> [0x4000 client session input data] -> [cjson or TLV parsing] -> [opcode specific 0x8000 buffer struct] -> [luci form data] -> [lua bytecode] -> [same 0x8000 buffer response data] -> [ 0x4000 client session output buffer ] -> [send()]

Continuing on, finally we can start talking about vulnerability-specific code. For this vulnerability, unlike all the others, we need to change the service_type value in the above bytes to 0xf1 from 0x1, such that we hit a different set of opcodes and can reach the function that handles opcode 0x1003, tpAPPMesh_addSlave:

0005018c    int32_t tpAppMesh_addSlave(struct psess_farg* inp)

000501a0        int32_t result = 0
000501a8        int32_t var_20 = 0
000501b0        struct cjson_obj* cjhash = nullptr
000501b8        void* s = nullptr
000501cc        int32_t entry_r2
000501cc        log("tpOneMesh.c:1198", "You are in onmesh process center of tpAppMesh_addSlave", entry_r2, 0)
000501cc        
000501d8        if (inp != 0)  
000501f4            if (tpAppMeshCheckAutoSync() != 0) // [13] 
0005021c                struct big_0x8000* inp_1
0005021c                int32_t r2
0005021c                // returned is big struct populated with wifi data
0005021c                // bug inm here
0005021c                inp_1, r2 = tpAPP_inf_parse_wifi_data(inp) // [14]
0005021c                

To start, there’s a configuration check at [13] to see if mesh auto-syncing is disabled, but by default this check passes, so we can ignore it. As such we hit the tpAPP_inf_parse_wifi_data function at [14] that actually parses our input data:

0004dc78    struct big_0x8000* tpAPP_inf_parse_wifi_data(struct psess_farg* inp)

0004dcd4        int32_t var_30_1
0004dcd4        __builtin_memset(dest: &var_30_1, ch: 0, count: 0x28)
0004dcf0        char inpcpy[0x400]
0004dcf0        memset(s: &inpcpy, c: 0, n: 0x800)
0004dd08        char const* const var_838 = "2.4G"
0004dd08        void* const var_834 = &data_625e8
0004dd0c        clear_bigstruct()
0004dd14        struct big_0x8000* result = &bigstruct
0004dd20        struct cjson_obj* inpcj
0004dd20        int32_t var_c_1
0004dd20        
0004dd20        if (inp == 0)
0004dd34            var_c_1 = 0xffffffff
0004dd48            log("tpOneMesh.c:321", "pSession or pSyncWifiParams is null.")
0004dd20        else
0004dd58            void* inpbuf = &inp->payload[8]
0004dd68            uint32_t inplen = inp->payload_len - 8
0004dd94            log("tpOneMesh.c:328", "%s %d: payloadLength %d", "tpAPP_inf_parse_wifi_data", 0x148, inplen)
0004ddac            log("tpOneMesh.c:329", ""%s"", inpbuf)
0004ddc8            memset(s: &inpcpy, c: 0, n: 0x400)
0004dde4            memcpy(dest: &inpcpy, src: inpbuf, n: inplen) // [15]
0004ddfc            inpcj = get_cjson_obj_from_inp(&inpcpy) // [16]
0004ddfc            

While our data is treated as a JSON string and converted to a CJSON object at [16], we don’t actually need to reach that far as there’s a copy of our ~0x4000 input data into a temporary stack buffer of size 0x400 and offset $sp-0x830 at [15]. This obviously leads to an out of bounds write on the stack and subsequent code execution.

Crash Information

Thread 2.1 "tmpServer" received signal SIGSEGV, Segmentation fault.
0x41414140 in ?? ()

[^_^] SIGSEGV

[o.O]> info reg
r0             0x0                 0
r1             0x51935             334133
r2             0x3c                60
r3             0x0                 0
r4             0x5159c             333212
r5             0x355010            3493904
r6             0x1                 1
r7             0x255ec             153068
r8             0x20                32
r9             0x0                 0
r10            0x1                 1
r11            0x41414141          1094795585
r12            0x1                 1
sp             0x7e9a6a18          0x7e9a6a18
lr             0x4de20             319008
pc             0x41414140          0x41414140
cpsr           0xa0000030          -1610612688
fpscr          <unavailable>
tpidruro       <unavailable>

[^.^]> bt
#0  0x41414140 in ?? ()
#1  0x0004de20 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
VENDOR RESPONSE

Vendor advisory: https://www.tp-link.com/us/support/faq/4943/

TIMELINE

2025-10-28 - Vendor Disclosure
2026-02-03 - Vendor Patch Release
2026-03-16 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.