Linux command-line client for a Qualcomm modem's EFS2 filesystem over the DIAG port (list/pull/push/backup)
Find a file
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
.gitignore Initial commit 2026-08-27 16:35:19 -05:00
efs2.py Initial commit 2026-08-27 16:35:19 -05:00
License.txt Initial commit 2026-08-27 16:35:19 -05:00
README.md Initial commit 2026-08-27 16:35:19 -05:00

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, 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.

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:

# 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 by JohnBel.