Single-file Python CLI for a Qualcomm modem's EFS2 filesystem over the DIAG serial port: ls, stat, pull, push, and recursive backup. Talks DIAG over pyserial to work around EfsTools' libnserial EIO on option-driver diag ports. Auto-detects the port by probing for a DIAG response.
88 lines
3.1 KiB
Markdown
88 lines
3.1 KiB
Markdown
# qcdm-efs2
|
|
|
|
A small command-line client for a Qualcomm modem's EFS2 filesystem, over the
|
|
DIAG serial port, in one file of Python. List, stat, pull, push, and back up
|
|
files in the modem's EFS from Linux, no Windows and no QPST.
|
|
|
|
I wrote it to back up and replace the RF band policy files (`/policyman`) on a
|
|
rooted Sony Xperia 1 VI, after finding that the usual tool,
|
|
[EfsTools](https://github.com/JohnBel/EfsTools), fails on Linux against the
|
|
`option`-driver diag port: its native serial layer sets modem control lines on
|
|
open and the driver rejects the ioctl with `EIO`. `pyserial` opens the same
|
|
port fine, so this talks DIAG over `pyserial` instead. The EFS2 packet layouts
|
|
are taken from EfsTools, so the wire format is identical; only the serial
|
|
handling differs.
|
|
|
|
The full write-up, including the Xperia band-unlock use case, is
|
|
[here](https://mrgecko.org/blog/2026/enable-us-5g-xperia-1-vi-from-linux).
|
|
|
|
## Requirements
|
|
|
|
- Python 3
|
|
- `pyserial` (`pip install pyserial`)
|
|
- A rooted phone with a Qualcomm modem, in DIAG mode
|
|
- Root on the Linux host to bind the serial driver
|
|
|
|
## Getting the phone onto a serial port
|
|
|
|
Put the phone into a USB composition that exposes the DIAG port (keeps adb):
|
|
|
|
```
|
|
adb shell su -c 'setprop sys.usb.config diag,serial_cdev,rmnet,adb'
|
|
```
|
|
|
|
It re-enumerates as a Qualcomm DIAG device (`05c6:9091`). The kernel does not
|
|
bind a serial driver on its own, so load `option` and hand it the USB id:
|
|
|
|
```
|
|
sudo modprobe option
|
|
echo 05c6 9091 | sudo tee /sys/bus/usb-serial/drivers/option1/new_id
|
|
```
|
|
|
|
That composition exposes a few serial nodes and only one is DIAG. You don't
|
|
have to work out which: `efs2.py` probes each candidate, sends a DIAG version
|
|
request, and keeps the one that answers. It looks at `/dev/serial/by-id/`
|
|
interface 0 (`-if00-`) first, then the rest. Set `DIAGPORT` or pass `--port` to
|
|
override.
|
|
|
|
## Usage
|
|
|
|
```
|
|
efs2.py [-h] [-p PORT] {ls,stat,pull,push,backup} ...
|
|
|
|
ls list an EFS directory
|
|
stat stat an EFS file
|
|
pull download one EFS file
|
|
push upload/overwrite one EFS file, then verify the read-back
|
|
backup recursively download an EFS directory
|
|
```
|
|
|
|
Examples:
|
|
|
|
```sh
|
|
# see what is in the modem's band-policy folder
|
|
python3 efs2.py ls /policyman
|
|
|
|
# back up a whole directory (subdirectories included) before changing anything
|
|
python3 efs2.py backup /policyman ./policyman-backup
|
|
|
|
# download and upload single files
|
|
python3 efs2.py pull /policyman/band_set_01.xml
|
|
python3 efs2.py push band_set_01.xml /policyman/band_set_01.xml
|
|
```
|
|
|
|
`push` deletes the target, re-creates it, writes in 1 KB chunks, then reads it
|
|
back and reports whether it matches. That read-back is the point: a truncated
|
|
write to the modem EFS is how you get a modem that won't register.
|
|
|
|
## Warning
|
|
|
|
This writes to the modem's EFS. A bad write to the wrong file can stop the
|
|
modem from booting or registering. Always `backup` first, and know your
|
|
recovery path (for Sony phones, reflashing the modem `.sin` files with
|
|
Newflasher restores stock EFS). Use at your own risk.
|
|
|
|
## License
|
|
|
|
MIT, see `License.txt`. EFS2 protocol layouts derived from
|
|
[EfsTools](https://github.com/JohnBel/EfsTools) by JohnBel.
|