A modern, memory-safe, and highly optimized Rust rewrite of the atcli and atcmd utilities for Qualcomm SDX55/X65/X75 modems (originally developed by Compal).
This project is the result of deep reverse engineering of the original OEM binaries. Our goal was to understand their internal mechanics, expose their critical vulnerabilities, and provide a standalone, static executable suitable for UBIFS embedded filesystems.
By disassembling the original C binaries (atcli and atcmd) using IDA/Ghidra, we uncovered how the OEM utilities interacted with the modem via the /dev/smd11 serial interface. We discovered two distinct architectures, both suffering from severe design flaws.
- The 4KB Global Buffer (Buffer Overflow Risk): The original code relied on a fixed 4096-byte global accumulator buffer (
byte_2410). It sent the AT command and used a loop withstpcpyto append every incoming chunk of data into this single buffer until it matched a terminator. If a command returned more than 4KB of data before hitting "OK" or "ERROR" (e.g., querying available network operators), it would overflow the buffer, potentially crashing the system. - Delayed Output: The utility waited for the entire response to be collected in the global buffer before printing anything to standard output via
puts().
- Stream Fragmentation Bug: The
atcmdvariant usedselect()followed byread()into a buffer, and then blindly usedstrstr(buffer, "OK")to check for completion. Because serial interfaces (like/dev/smd11) are streams, a response likeOK\r\ncan be fragmented across two separateread()calls (e.g., readingOthenK). Thestrstrlogic fails to catch this, causing the program to hang. - Hardcoded 5-Second Timebomb: Instead of waiting dynamically for a response, the main thread spawned a background reading thread (
pthreads), issued the AT command, and then triggered a hardcodedusleep(5000000)(exactly 5 seconds). If the modem took 6 seconds to process a complex command, the main thread would wake up and violently terminate the program, cutting off the response. - Hardware Flow Control Hacks: The code manually forced the RTS (Ready To Send) pin using the
TIOCMBICioctl (0x5416). Modern Linux TTY drivers typically handle this automatically.
This Rust version merges the best of both worlds while addressing all OEM architectural flaws:
- Line-by-Line Streaming: Instead of accumulating the whole response in a global buffer, this version uses
BufReader::read_line. It processes and prints the output dynamically line-by-line. This means memory usage is always O(1) (only needing enough RAM for a single line), eliminating any buffer overflow risk. - Stream-Safe Matching: By reading until the
\ncharacter, we naturally bypass the serial fragmentation bugs that plagued thestrstr()implementation in the original C code. - No Threading Bloat or Arbitrary Timeouts: We use a highly efficient synchronous blocking read. No
pthreads, no hardcoded 5-second timeouts. The program waits exactly as long as the modem needs. - Dynamic Port Selection: You can seamlessly switch between the default
/dev/smd11and custom ports (e.g.,/dev/ttyUSB0) using the-pflag. - Fully Static & Tiny: Compiled with
+crt-static, it relies on zero shared libraries from the router's OS.
To build this for the ARMv7 architecture (used by SDX55 modems), you need the Rust toolchain and the appropriate target. See BUILD.md for details
This project is licensed under the GNU Lesser General Public License v2.1 - see the LICENSE file for details.