Summary
ippon_command() in drivers/nutdrv_qx.c can read past the end of its 64-byte stack reply buffer. A full-length USB reply containing neither a carriage return (0x0D) nor a NUL byte (0x00) overwrites the complete zero-initialized buffer and leaves it unterminated. The bounded carriage-return scan then finds nothing, and the fallback calls strlen() on that buffer.
The resulting out-of-bounds length is also passed to upsdebug_hex() before it is clamped, and the following text debug statement independently calls unbounded strcspn() on the same buffer. Depending on the surrounding stack contents, this can crash the driver or read adjacent stack bytes into debug output and the caller's reply buffer. I have not established a write primitive or code execution.
Triggering the condition requires a faulty or attacker-controlled USB device, or an emulator, selected for the Ippon transport to return the malformed full-length reply.
Affected versions
The vulnerable fallback was introduced by commit b707432942e396db82b9191da1bc3694b7ea4950 on 2014-09-03. It is present in
the stable tags v2.7.3, v2.7.4, and v2.8.0 through v2.8.5, as well as the reviewed master baseline 26177060be94a738bf2d612441af264c90ac37a9. The first affected stable release therefore appears to be v2.7.3.
The USB ID table automatically selects this transport for Phoenixtec 06da:0003, documented there as Mustek Powermust. It can also be selected explicitly with subdriver = ippon.
Details
The reply buffer is:
It is initially cleared for command transmission, but a successful full-size
read overwrites all 64 bytes:
ret = usb_interrupt_read(udev,
0x81,
(usb_ctrl_charbuf)tmp, sizeof(tmp), 1000);
The carriage-return scan itself is bounded by ret:
for (i = 0, len = 0; i < (size_t)ret; i++) {
if (tmp[i] != '\r')
continue;
len = ++i;
break;
}
If a 64-byte reply contains no carriage return, len remains zero. If it also
contains no NUL byte, this fallback reads beyond tmp:
if (!len)
len = strlen(tmp);
The out-of-bounds result is then used before any clamp:
upsdebug_hex(5, "read", tmp, (size_t)len);
upsdebugx(3, "read: %.*s", (int)strcspn(tmp, "\r"), tmp);
upsdebug_hex() may therefore read and log bytes beyond tmp. The strcspn() call independently scans beyond the buffer until it happens to find a carriage return or NUL byte.
Only afterwards does the code limit the copy length:
len = len < buflen ? len : buflen - 1;
memset(buf, 0, buflen);
memcpy(buf, tmp, len);
The normal caller, qx_process(), passes a 511-byte destination. A strlen() result between 65 and that destination limit can therefore copy adjacent stack bytes into the caller's reply buffer. The clamp does not restore the source bound and does not protect either debug read above.
PoC
I do not have Ippon hardware or an emulated proof of concept and have not tried to demonstrate a crash or memory disclosure on a live system.
The condition follows directly from the source when usb_interrupt_read() returns 64 bytes, all non-zero and none equal to 0x0D: the bounded scan leaves len == 0, and strlen(tmp) receives a non-terminated 64-byte array.
A focused reproduction could mock usb_interrupt_read() to return 64 bytes with no carriage return and no NUL, then call ippon_command() under ASan or Valgrind. I can prepare such a test if it would help triage.
Impact
This is an out-of-bounds read (CWE-125). Exploitation requires control of, emulation of, or a fault in the attached USB device selected for the Ippon transport. Potential consequences are:
- a driver crash, disrupting UPS monitoring; and
- disclosure of adjacent driver stack bytes through debug output or data copied into the caller's reply buffer.
No memory write outside the intended destination and no code execution have been demonstrated.
Suggested direction
When no carriage return is found, determine the payload length only within the ret bytes: use a bounded NUL search such as memchr(tmp, '\0', ret), or treat all ret bytes as payload if neither terminator is present. Use that bounded length for hex/text logging and for the destination copy. A regression test should cover a 64-byte reply containing neither carriage return nor NUL.
Discovery context and AI disclosure
This was found while deriving a separate OMRON USB transport for the same driver. The OMRON implementation uses bounded memchr()/length handling and does not modify ippon_command().
AI assistance from Claude and OpenAI Codex was used during discovery and drafting. The source-level control flow, affected-version history and this report were reviewed and are submitted by the human reporter.
Summary
ippon_command()indrivers/nutdrv_qx.ccan read past the end of its 64-byte stack reply buffer. A full-length USB reply containing neither a carriage return (0x0D) nor a NUL byte (0x00) overwrites the complete zero-initialized buffer and leaves it unterminated. The bounded carriage-return scan then finds nothing, and the fallback callsstrlen()on that buffer.The resulting out-of-bounds length is also passed to
upsdebug_hex()before it is clamped, and the following text debug statement independently calls unboundedstrcspn()on the same buffer. Depending on the surrounding stack contents, this can crash the driver or read adjacent stack bytes into debug output and the caller's reply buffer. I have not established a write primitive or code execution.Triggering the condition requires a faulty or attacker-controlled USB device, or an emulator, selected for the Ippon transport to return the malformed full-length reply.
Affected versions
The vulnerable fallback was introduced by commit
b707432942e396db82b9191da1bc3694b7ea4950on 2014-09-03. It is present inthe stable tags
v2.7.3,v2.7.4, andv2.8.0throughv2.8.5, as well as the reviewedmasterbaseline26177060be94a738bf2d612441af264c90ac37a9. The first affected stable release therefore appears to bev2.7.3.The USB ID table automatically selects this transport for Phoenixtec
06da:0003, documented there as Mustek Powermust. It can also be selected explicitly withsubdriver = ippon.Details
The reply buffer is:
It is initially cleared for command transmission, but a successful full-size
read overwrites all 64 bytes:
The carriage-return scan itself is bounded by
ret:If a 64-byte reply contains no carriage return,
lenremains zero. If it alsocontains no NUL byte, this fallback reads beyond
tmp:The out-of-bounds result is then used before any clamp:
upsdebug_hex()may therefore read and log bytes beyondtmp. Thestrcspn()call independently scans beyond the buffer until it happens to find a carriage return or NUL byte.Only afterwards does the code limit the copy length:
The normal caller,
qx_process(), passes a 511-byte destination. Astrlen()result between 65 and that destination limit can therefore copy adjacent stack bytes into the caller's reply buffer. The clamp does not restore the source bound and does not protect either debug read above.PoC
I do not have Ippon hardware or an emulated proof of concept and have not tried to demonstrate a crash or memory disclosure on a live system.
The condition follows directly from the source when
usb_interrupt_read()returns 64 bytes, all non-zero and none equal to0x0D: the bounded scan leaveslen == 0, andstrlen(tmp)receives a non-terminated 64-byte array.A focused reproduction could mock
usb_interrupt_read()to return 64 bytes with no carriage return and no NUL, then callippon_command()under ASan or Valgrind. I can prepare such a test if it would help triage.Impact
This is an out-of-bounds read (CWE-125). Exploitation requires control of, emulation of, or a fault in the attached USB device selected for the Ippon transport. Potential consequences are:
No memory write outside the intended destination and no code execution have been demonstrated.
Suggested direction
When no carriage return is found, determine the payload length only within the
retbytes: use a bounded NUL search such asmemchr(tmp, '\0', ret), or treat allretbytes as payload if neither terminator is present. Use that bounded length for hex/text logging and for the destination copy. A regression test should cover a 64-byte reply containing neither carriage return nor NUL.Discovery context and AI disclosure
This was found while deriving a separate OMRON USB transport for the same driver. The OMRON implementation uses bounded
memchr()/length handling and does not modifyippon_command().AI assistance from Claude and OpenAI Codex was used during discovery and drafting. The source-level control flow, affected-version history and this report were reviewed and are submitted by the human reporter.