qcdm-efs2/README.md
James Coleman 309997a175 Initial commit
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.
2026-08-27 16:35:19 -05:00

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.