PFB-005Vendor: Prusa ResearchProduct: Buddy firmware
M919/M920 write one byte past a 16-byte stack buffer from a scanf width off by one
- Severity
- CVSS 5.4Medium
CVSS:3.1/, FIRST CVSS calculator (opens in a new tab)AV:N/ AC:L/ PR:L/ UI:N/ S:U/ C:N/ I:L/ A:L CVSS metrics and CWE
- Attack Vector: Network (worst case)
- Attack Complexity: Low (worst case)
- Privileges Required: Low
- User Interaction: None (worst case)
- Scope: Unchanged
- Confidentiality: None
- Integrity: Low
- Availability: Low
- CVE
- Not yet assigned
- Affected models
- CORE One L+
- CORE One L
- CORE One+ (Gen 2) INDX
- CORE One INDX
- CORE One+ (Gen 2)
- CORE One
- XL+
- XL
- MK4S
- MK4
- MK3.9S
- MK3.9
- MK3.5S
- MK3.5
- MINI
- Fixed in
- 7.0.0
- Attack surface
- Authenticated PrusaLink or Connect upload, USB print file, USB serial
- Verification
- Reproduced on host
AddressSanitizer-confirmed on the host and source-verified. Clang
-Wfortify-sourceflags it statically. Not hardware-tested: program-counter control is not demonstrated. - Analysis notes
Versions and source tree
Prusa Buddy firmware, MK4S. We verified the defect in the 6.6.0 source tree (commit e96ce2b9). The reference printer runs 6.5.7.
- Disclosed in
- Never gonna print you up!
Summary
PrusaGcodeSuite::M919 and M920 scan a token from the current G-code line into a 16-byte stack buffer with the format %16s.
In scanf, the width is the count of characters that the function converts.
The function writes the null terminator after these characters.
So %16s needs 17 bytes for a 16-byte buffer.
A G-code line whose argument opens with 16 non-whitespace characters writes one byte past the buffer.
The argument is parser.string_arg, which is the current line.
So the trigger is any G-code that the printer executes over serial or from a print file.
On the host, AddressSanitizer confirms this out-of-bounds stack write. We did not test it on hardware, and we did not demonstrate a controlled program counter.
The defect
M919 declares a 16-byte buffer on the stack and reads the token into it with a scanf width of 16:
// src/marlin_stubs/M919-M920.cpp:62-70
char cmd[16] = { 0 };
int val = 0;
...
int read = sscanf(parser.string_arg, "%16s %x", cmd, &val);A scanf width counts the converted characters, and scanf writes the null terminator after that count.
So %16s can convert 16 characters and then write a seventeenth byte for the terminator, while cmd holds only 16.
The widest conversion that fits a char[16] buffer is %15s.
M920 has the same shape, with a 16-byte buffer and a width-16 read:
// src/marlin_stubs/M919-M920.cpp:123-130
char cmd[16] = { 0 };
...
int read = sscanf(parser.string_arg, "%16s", cmd);Reaching it
The attacker position is a single line of G-code that the printer executes. The dispatcher calls both handlers inside one compile guard:
// src/marlin_stubs/gcode.cpp:243-248
#if HAS_TRINAMIC
case 919:
PrusaGcodeSuite::M919();
break;
case 920:
PrusaGcodeSuite::M920();HAS_TRINAMIC (gcode.cpp:243) is true for every printer in this tree, because they all have TMC stepper drivers.
Dispatch runs from process_next_command to process_parsed_command (lib/Marlin/Marlin/src/gcode/gcode.cpp:761) and then to process_parsed_command_custom (:771).
The command queue takes its lines from the immediate, serial, and sd-card injectors alike (queue.cpp:54-55).
So a line in a print file reaches the handler in the same way as a line typed over USB.
The scenario that matches the header score, network-reachable at low privilege, is the strongest realistic one. An authenticated PrusaLink or Prusa Connect user uploads a G-code file that carries the line, and starts the print. The line overflows the buffer when the printer executes it.
Two weaker positions reach the same code.
A file printed from a USB stick reaches it physically, and the owner starts the print.
M919 typed straight over the USB serial line reaches it with no file and no print.
This position scores CVSS 3.5 (Low) at AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L.
The command runs at print time, so previewing or opening the file does not trigger it.
Impact
The write lands one byte past a 16-byte stack buffer.
That byte is the null terminator.
So the attacker chooses when the write happens but does not choose the value.
Where the byte lands depends on the compiler's frame layout at -Os for the STM32 target.
On the host, the AddressSanitizer build places it in the redzone ahead of the adjacent val.
In the MK4S 6.5.7 image, which the reference printer runs, both handlers are inlined into the dispatcher, and in emulation the byte lands in a slot of the dispatcher's stack frame.
In emulation of the INDX 6.9.2 image, M919's byte lands on the low byte of a register value that M919 saves on its stack at entry and restores for the dispatcher when it returns.
On the same image, in emulation, M920's byte lands in a slot of the dispatcher's stack frame.
In the INDX and MK4S 7.0.0 images, both handlers clear 17 bytes for the buffer, and in emulation nothing lands outside them.
We demonstrated an out-of-bounds stack write.
We did not demonstrate control-flow hijacking, and one fixed null byte at a compiler-chosen offset is a weak primitive for it.
From the machine code and from emulation, we infer that nothing uses the overwritten byte on MK4S 6.5.7 or INDX 6.9.2.
No TMC register name reaches 16 characters (lib/Marlin/Marlin/src/feature/tmc_util.cpp:1273-1338), so a token long enough to overflow always takes the handler's error path.
On that path, neither image's dispatcher reads the overwritten stack slot before it returns.
On INDX 6.9.2, nothing uses the register that M919 hands back before the dispatcher restores its own saved copy on return.
So we do not expect this byte to crash the firmware on either image, but we did not test that on hardware.
Other builds can place the byte elsewhere, and we examined only these two affected shipped images, so the vector rates availability at A:L.
This defect rates above an out-of-bounds read because it is a stack write and a single line of a print file reaches it. PFB-007 reaches a controlled program counter on hardware. This advisory does not.
Proof
A small host program declares the same 16-byte cmd and makes the same sscanf(..., "%16s %x", cmd, &val) call, with the token ABCDEFGHIJKLMNOP at exactly 16 characters.
We compiled it with AddressSanitizer.
AddressSanitizer reports a stack-buffer-overflow, a WRITE of size 17 into the 16-byte cmd.
The same source compiled with -D_FORTIFY_SOURCE=2 warns at compile time that the destination buffer has size 16 but the specifier may require size 17 [-Wfortify-source].
On the printer, the line that triggers it is:
M919 X ABCDEFGHIJKLMNOP 1234Source release
Prusa published the 7.0.0 sources under the v7.0.0 release tag on GitHub.