Qualcomm modem firmware is very large, which they have tried different types of compression of increasing complexity to try and mitigate: Delta/DLpager, q6izp, CLADE and CLADE2. I keep forgetting how to decompress these, so I'm going to walk through how I go about decompressing them, starting with Delta compression. This algorithm isn't used as much on newer modems, but is used commonly on older ones for data compression (with code sections using q6zip).
I'm going to be using a OnePlus 6 modem for this (OnePlus6Oxygen_22.J.62_OTA_0620_all_2111252336_14afec75dd6fa), I won't explain the process of extracting the modem.img from the factory image here. Once you've extracted the modem partition, which contains the modem binary split into sections, you can stitch the modem.img back together with this script. Load this into Ghidra using ghidra-hexagon-sleigh for Hexagon language support.
The delta compressed data is stored in its own section in the binary, mapped as readable, writable and non-executable. This address is also stored in the compression parameters struct, which can be found by searching for
ffffffff 00000000 00000000 <section address>
For my firmware this section address is 0xc69ea000. The compression parameters struct, which in my firmware can be found at 0xc2bf1bec (with the field we are interested in delta_section at 0xc2bf1bf8), is a section bounded by ffffffff which contains various parameters used for decompression. It looks like this on my firmware (the layout is slightly different depending on the specific firmware you are looking at):

You will find the output address delta_out_addr at the end of the struct, this is 0xd05ed000 in my firmware.
This global should have a single cross-reference to a function that uses these fields (that doesn't decompile well) at 0xc04d6258. Going up the call stack from this function, the parent (decompress_setup_setup_parent, 0xc04d5ca4) looks something like this:
undefined4 decompress_setup_setup_parent(void) {
if (DAT_c30f7118 != 0) {
return 0;
}
DAT_c30f7118 = 1;
memscpy("03.02.00",8,"03.02.00",8);
horrible(); // <----- Function that uses the delta_section
FUN_c04d6840();
decompress_setup_setup();
//..
}
decompress_setup_setup looks like this:
undefined4 decompress_setup_setup(void) {
DAT_c476ca90 = 0;
init_sem(&DAT_c476ca98,0,0);
init_sem(&UNK_c476caa8,0);
FUN_c04d6cc4();
FUN_d921ad6c((void *)(**(int **)(in_GP + 0x1df3) + 4)); // <--- Here
return 0;
}
And following the last function:
void FUN_d921ad6c(void *param_1) {
q6zip_dict = param_1;
DAT_c40818d4 = in_r1;
DAT_c40818d8 = in_r3;
init_sem();
FUN_d921afe8(decompress_stuff); // <--- Here
//..
}
Here we find decompress_stuff which is the function we've been looking for. This function handles both q6zip and delta decompression. We're specifically interested in the part where *param_1 == 2, which looks like this:
// ..
uVar10 = uVar7 & 0x1f;
uVar11 = uVar10 + (int)((ulonglong)*(undefined8 *)(param_1 + 4) >> 0x20) + 0x1f;
l2fetch(uVar7 - uVar10,(ulonglong)CONCAT24(0x20,uVar11 >> 5 & 0xffff | 0x200000));
uVar2 = delta_decompress(*(undefined4 *)(param_1 + 4),*(undefined4 *)(param_1 + 0xc),
*(uint *)(param_1 + 0x10) >> 2);
*(undefined4 *)(param_1 + 0x24) = uVar2;
// ..
With this we know that the delta_decompress function is at d921ba30.
Now we can emulate this decompression code with Ghidra's PCode emulator. My Hexagon plug ghidra-hexagon-sleigh already has a script to do this, called dlpager_emu.py that just needs the parameters we've retrieved. Edit it to add
OUT_BUF = 0xd05ed000
IN_BUF = 0xc69ea000
DECOMPRESS_FUNCTION = 0xd921ba30
Create the output section in the memory map and run it.
The result will look something like this:
