[PATCH v2] kgdb: Fix buffer overflow in the 'M' and 'X' packet handlers

Fang Xieyan posted 1 patch 1 day, 22 hours ago
kernel/debug/gdbstub.c | 45 +++++++++++++++++++++++++++++++++++-------
1 file changed, 38 insertions(+), 7 deletions(-)
[PATCH v2] kgdb: Fix buffer overflow in the 'M' and 'X' packet handlers
Posted by Fang Xieyan 1 day, 22 hours ago
write_mem_msg() passes the length claimed by an 'M' or 'X' packet directly
to kgdb_hex2mem() or kgdb_ebin2mem(). A malformed packet can claim more
data than was actually received, causing the decoders to read past
remcom_in_buffer.

Record the received payload length in get_packet() and use it to bound
the decoders. 'M' carries two hex characters per byte, while 'X' carries
one byte per output byte except that a 0x7d escape consumes an additional
input byte. Pass the packet end to kgdb_ebin2mem() so both reads are
checked against the received data.

Keep zero-length 'X' packets valid, as GDB uses them to probe binary
download support.

Fixes: dc7d55270521 ("kgdb: core")
Cc: stable@vger.kernel.org
Assisted-by: Hawkeye:GLM-5.3-flash
Assisted-by: Qoder:Qwen3.8-Max
Signed-off-by: Fang Xieyan <fangxy@xiaopeng.com>
---

Changes in v2:
  - Bound the decode by the bytes actually received instead of a fixed
    divisor. get_packet() now returns the received count and
    write_mem_msg() uses it as the end of the payload.
  - Split the 'M' and 'X' bounds. 'X' is no longer capped at BUFMAX / 2,
    so a valid binary write between BUFMAX / 2 and BUFMAX - header is
    accepted again; v1 rejected it with E22, and GDB aborts a write
    that gets E22 rather than falling back to 'M'.
  - Give kgdb_ebin2mem() a buf_end argument and check it before both
    reads in the loop, so a 0x7d escape cannot read past the received
    data.
  - v1: https://lore.kernel.org/all/20260916080306.13395-1-fangxy@xiaopeng.com/

Write-side sibling of the 'm' memread overflow in gdb_cmd_memread()
(remcom_out_buffer); both handlers trust the same unvalidated length
field. The read-side fix was posted separately:
https://lore.kernel.org/all/20260914180344.15485-1-fangxy@xiaopeng.com/

Reproducer ('M', x86 BUFMAX=1024): send "M<addr>,200:4142434445464748"
on a kgdboc console. The stub decodes 2 * 512 bytes from an 8-byte
payload, so the in-place hex conversion runs off remcom_in_buffer.
Unpatched: KASAN global-out-of-bounds read in kgdb_hex2mem at
remcom_in_buffer+0x415/0x420, guest panics. Patched: $E22, guest
resumes. A minimal over-claim reproduces best: a huge length smashes
past the buffer through the uninstrumented copy in
copy_to_kernel_nofault() with no report at all.

'X' cases (patched kernel answers shown; x86, same harness):
  - over-claim "X<addr>,400:" with an 8-byte body: $E22. Unpatched this
    splats in write_mem_msg (kgdb_ebin2mem inlined) at
    remcom_in_buffer+0x400/0x420 and the guest dies.
  - well-formed 900-byte write "X<addr>,384:": $OK, accepted again.
    This is the packet v1 wrongly rejected; the write lands on
    init_task, so the harness asserts $OK without resuming the guest.
  - lone trailing escape "X<addr>,40:": $E22.
  - zero-length "X0,0:": $OK (GDB's binary-download probe).

Built and run on 704340f1cd0d (9 commits past v7.3-rc3), x86_64
defconfig + CONFIG_KASAN_GENERIC, gcc 13.2.0, QEMU under TCG; the
before/after kernels are built from byte-identical .config files and
differ only by this patch. CONFIG_KGDB_KDB has to be off so the packet
is parsed by the gdb stub rather than kdb. An unrelated early-boot
"wild-memory-access in _raw_spin_lock" TCG+KASAN flake can reboot the
guest before the break-in; re-running the boot clears it.

 kernel/debug/gdbstub.c | 45 +++++++++++++++++++++++++++++++++++-------
 1 file changed, 38 insertions(+), 7 deletions(-)

diff --git a/kernel/debug/gdbstub.c b/kernel/debug/gdbstub.c
index e271a43..0293524 100644
--- a/kernel/debug/gdbstub.c
+++ b/kernel/debug/gdbstub.c
@@ -43,6 +43,8 @@
 /* Our I/O buffers. */
 static char			remcom_in_buffer[BUFMAX];
 static char			remcom_out_buffer[BUFMAX];
+/* Payload bytes get_packet() stored in remcom_in_buffer, excluding the NUL. */
+static int			remcom_in_len;
 static int			gdbstub_use_prev_in_buf;
 static int			gdbstub_prev_in_buf_pos;
 
@@ -87,7 +89,7 @@ static int gdbstub_read_wait(void)
 }
 #endif
 /* scan for the sequence $<data>#<checksum> */
-static void get_packet(char *buffer)
+static int get_packet(char *buffer)
 {
 	unsigned char checksum;
 	unsigned char xmitcsum;
@@ -135,6 +137,8 @@ static void get_packet(char *buffer)
 		}
 		buffer[count] = 0;
 	} while (checksum != xmitcsum);
+
+	return count;
 }
 
 /*
@@ -321,16 +325,26 @@ int kgdb_hex2long(char **ptr, unsigned long *long_val)
  * Copy the binary array pointed to by buf into mem.  Fix $, #, and
  * 0x7d escaped with 0x7d. Return -EFAULT on failure or 0 on success.
  * The input buf is overwritten with the result to write to mem.
+ *
+ * buf_end points one past the last byte received for this packet. Each
+ * 0x7d escape consumes a second input byte, so decoding count bytes can
+ * read up to 2 * count input bytes; stop at buf_end rather than running
+ * off the end of remcom_in_buffer.
  */
-static int kgdb_ebin2mem(char *buf, char *mem, int count)
+static int kgdb_ebin2mem(char *buf, char *mem, int count, char *buf_end)
 {
 	int size = 0;
 	char *c = buf;
 
 	while (count-- > 0) {
+		if (buf >= buf_end)
+			return -EINVAL;
 		c[size] = *buf++;
-		if (c[size] == 0x7d)
+		if (c[size] == 0x7d) {
+			if (buf >= buf_end)
+				return -EINVAL;
 			c[size] = *buf++ ^ 0x20;
+		}
 		size++;
 	}
 
@@ -367,16 +381,33 @@ void gdb_regs_to_pt_regs(unsigned long *gdb_regs, struct pt_regs *regs)
 static int write_mem_msg(int binary)
 {
 	char *ptr = &remcom_in_buffer[1];
+	char *buf_end = &remcom_in_buffer[remcom_in_len];
 	unsigned long addr;
 	unsigned long length;
 	int err;
 
 	if (kgdb_hex2long(&ptr, &addr) > 0 && *(ptr++) == ',' &&
 	    kgdb_hex2long(&ptr, &length) > 0 && *(ptr++) == ':') {
-		if (binary)
-			err = kgdb_ebin2mem(ptr, (char *)addr, length);
-		else
+		/*
+		 * Trust only the bytes actually received, not length: the
+		 * client can claim more payload than it sent. 'M' decodes two
+		 * hex chars per byte in place, so it needs 2 * length bytes at
+		 * ptr; 'X' decodes one byte per byte, and kgdb_ebin2mem() bounds
+		 * its extra 0x7d-escape reads against buf_end itself.
+		 */
+		if (ptr >= buf_end)
+			return -EINVAL;
+
+		if (binary) {
+			if (length > (unsigned long)(buf_end - ptr))
+				return -EINVAL;
+			err = kgdb_ebin2mem(ptr, (char *)addr, length, buf_end);
+		} else {
+			if (length > (unsigned long)(buf_end - ptr) / 2)
+				return -EINVAL;
 			err = kgdb_hex2mem(ptr, (char *)addr, length);
+		}
+
 		if (err)
 			return err;
 		if (CACHE_FLUSH_IS_SAFE)
@@ -985,7 +1016,7 @@ int gdb_serial_stub(struct kgdb_state *ks)
 		/* Clear the out buffer. */
 		memset(remcom_out_buffer, 0, sizeof(remcom_out_buffer));
 
-		get_packet(remcom_in_buffer);
+		remcom_in_len = get_packet(remcom_in_buffer);
 
 		switch (remcom_in_buffer[0]) {
 		case '?': /* gdbserial status */
-- 
2.50.1 (Apple Git-155)
Re: [PATCH v2] kgdb: Fix buffer overflow in the 'M' and 'X' packet handlers
Posted by Doug Anderson 1 day, 14 hours ago
Hi,

On Tue, Sep 22, 2026 at 8:49 AM Fang Xieyan <fangxy@xiaopeng.com> wrote:
>
> write_mem_msg() passes the length claimed by an 'M' or 'X' packet directly
> to kgdb_hex2mem() or kgdb_ebin2mem(). A malformed packet can claim more
> data than was actually received, causing the decoders to read past
> remcom_in_buffer.
>
> Record the received payload length in get_packet() and use it to bound
> the decoders. 'M' carries two hex characters per byte, while 'X' carries
> one byte per output byte except that a 0x7d escape consumes an additional
> input byte. Pass the packet end to kgdb_ebin2mem() so both reads are
> checked against the received data.
>
> Keep zero-length 'X' packets valid, as GDB uses them to probe binary
> download support.

My analysis shows that the above isn't true and that zero-length 'X'
packets are no longer valid with your patch. I haven't tested this
myself, though. Yell if I got it wrong.


> @@ -43,6 +43,8 @@
>  /* Our I/O buffers. */
>  static char                    remcom_in_buffer[BUFMAX];
>  static char                    remcom_out_buffer[BUFMAX];
> +/* Payload bytes get_packet() stored in remcom_in_buffer, excluding the NUL. */
> +static int                     remcom_in_len;

I don't love making this a global. Can we just pass it down? ...and
maybe for functions where you pass the length you also pass
"remcom_in_buffer" as a parameter and stop accessing the global?

-Doug
Re: [PATCH v2] kgdb: Fix buffer overflow in the 'M' and 'X' packet handlers
Posted by Fang Xieyan 1 day, 5 hours ago
Hi Doug,

Thanks for the review.

> My analysis shows that the above isn't true and that zero-length 'X'
> packets are no longer valid with your patch. I haven't tested this
> myself, though. Yell if I got it wrong.

You're right. For "X0,0:", ptr reaches buf_end after parsing ':',
and the `ptr >= buf_end` check returns -EINVAL before the length
check. I'll change it to `ptr > buf_end` so zero-length 'X' packets
remain valid.

> I don't love making this a global. Can we just pass it down? ...and
> maybe for functions where you pass the length you also pass
> "remcom_in_buffer" as a parameter and stop accessing the global?

My plan is to pass both remcom_in_buffer and the received length
through the relevant functions:

  gdb_serial_stub() -> gdb_cmd_memwrite() / gdb_cmd_binwrite()
                    -> write_mem_msg()

so that write_mem_msg() no longer accesses remcom_in_buffer
directly. These functions are all static and only used within
gdbstub.c, so the signature changes stay local.

Re: [PATCH v2] kgdb: Fix buffer overflow in the 'M' and 'X' packet handlers
Posted by Fang Xieyan 1 day, 3 hours ago
Hi Doug,

One correction to my previous reply: after checking the packet
parsing more closely, I don't think an explicit `ptr > buf_end`
check is needed.

I'll keep the v3 implementation without this redundant check.

Thanks