Skip to content

cmd_program() returns True when the device rejected the <program> command — qfil reports success while writes are silently dropped #809

Description

@suddenBook

Environment

edlclient 3.62 (Arch AUR edl-git 3.52.1.r428.g51e1102-1, commit 51e1102)
Python 3.14
Host OS Linux 7.1.5 (CachyOS)
Connection USB (usblib.py), VID:PID 05c6:9008
Device Lenovo TB390FU, UFS, 8 LUNs, 4096-byte sectors, prod_name=HN8T174EJKX075
Command edl --loader=xbl_s_devprg_ns.melf --memory=ufs qfil <rawprogram list> <patch list> <imagedir>

Summary

If the device does not accept the <program> command, cmd_program() skips the entire write loop and
still returns True. qfil has no way to know, prints [qfil] raw programming ok., exits 0, and
the image is simply not on the device.

In one 127-entry qfil run on my device, 4 of the 12 GPT writes were silently dropped. I only
found out by reading the tables back off the device and diffing them against the source images.
Nothing in edl's output indicated a problem.

The code

Library/firehose.py:473-534

def cmd_program(self, physical_partition_number, start_sector, filename, display=True):
    ...
    rsp = self.xmlsend(data, self.skipresponse)
    progbar.show_progress(prefix="Write", pos=0, total=total, display=display)
    if rsp.resp:                                  # <-- if falsy, everything below is skipped
        while bytestowrite > 0:
            ...
            self.cdc.write(wdata)
            progbar.show_progress(...)
            self.cdc.write(b'')
        wd = self.wait_for_data()
        log = self.xml.getlog(wd)
        rsp = self.xml.getresponse(wd)
        if "value" in rsp:
            if rsp["value"] != "ACK":
                self.error(f"Error:")
                for line in log:
                    self.error(line)
                return False
        else:
            self.error(f"Error:{rsp}")
            return False
    #  <-- no `else:` branch at all
    return True                                   # <-- reports success unconditionally

There is no else for if rsp.resp:, and no error is logged on that path.

cmd_program_buffer() (firehose.py:536-593) has the same shape. It does log
self.error(f"Error:{rsp.error}") in its else, but then still falls through to return True on
line 593.

qfil (firehose_client.py:962) calls it and discards the result entirely:

self.firehose.cmd_program(int(partition_number), int(start_sector), filename)

Log signature

A dropped write is indistinguishable from a successful one except that the progress bar never
advances past the initial show_progress(pos=0) call. All four failures in my run look exactly like
this — one 0.0% line, then straight on to the next file, no error:

firehose_client - [qfil] programming .../gpt_main0.bin to partition(0)@sector(0)...
firehose -
Writing to physical partition 0, sector 0, sectors 6
Done |----------|   0.0% Write (Sector 0x0 of 0x6) 0.00 MB/s
firehose_client - [qfil] programming .../gpt_backup0.bin to partition(0)@sector(59293691)...

versus a successful one:

firehose_client - [qfil] programming .../gpt_main2.bin to partition(2)@sector(0)...
firehose -
Writing to physical partition 2, sector 0, sectors 6
Done |----------|   0.0% Write (Sector 0x0 of 0x6) 0.00 MB/s
Progress: |██████████| 100.0% Write (Sector 0x6 of 0x6, ) 120.32 MB/s

Because progress lines are written with \r, the 0.0% line is usually overwritten on a terminal and
the user sees nothing at all.

Evidence

Which writes landed, determined by reading sectors back off the device with rs and comparing the
partition arrays against the source gpt_main*.bin / gpt_backup*.bin:

write edl said actually landed
gpt_main0.bin → LUN0 sector 0 ok no
gpt_backup0.bin → LUN0 sector 59293691 ok yes
gpt_main2.bin → LUN2 sector 0 ok yes
gpt_backup2.bin → LUN2 sector 5115 ok no
gpt_main3.bin → LUN3 sector 0 ok no
gpt_backup3.bin → LUN3 sector 8187 ok yes
gpt_main5.bin → LUN5 sector 0 ok yes
gpt_backup5.bin → LUN5 sector 8187 ok no

qfil finished with [qfil] raw programming ok., [qfil] patching ok, and exit status 0.

The dropped GPT writes left the device with primary and backup tables that disagreed: same partition
names and LBA ranges, but different unique_guid on every entry, different type_guid on 58 of 77
entries on LUN4, and different A/B slot attribute bytes (e.g. abl_a primary
0x1000000000000000 vs backup 0x10c0000000000000). Both tables were internally CRC-valid, so no
consistency check on the device would flag it.

Related: the qfil patch phase also ignores responses

Library/firehose_client.py:974-988

content = ElementTree.tostring(elem).decode("utf-8")
CMD = "<?xml version=\"1.0\" ?><data>\n {content} </data>".format(content=content)
print(CMD)
self.firehose.xmlsend(CMD)          # <-- return value discarded

I hit this too. After writing a fresh gpt_main0.bin, the patch that fixes up the last partition's
size —

<patch SECTOR_SIZE_IN_BYTES="4096" byte_offset="2216" filename="DISK"
       physical_partition_number="0" size_in_bytes="8" start_sector="2"
       value="NUM_DISK_SECTORS-6."
       what="Update last partition 18 'userdata' with actual size in Primary Header."/>

— did not take effect, with no diagnostic. The result was a structurally valid GPT (both CRCs
correct) in which userdata had first_lba=6419880, last_lba=6419879, i.e. a zero-length
partition
. A GPT like that will pass any signature/CRC check and then fail to mount.

The print(CMD) on line 987 also looks like leftover debugging — it dumps every patch XML to stdout.

Suggested fix

Minimally, stop reporting success. This applies cleanly to 51e1102 and compiles:

--- a/edlclient/Library/firehose.py
+++ b/edlclient/Library/firehose.py
@@ -531,6 +531,10 @@
                 else:
                     self.error(f"Error:{rsp}")
                     return False
+            else:
+                self.error(f"Error: device rejected the program command for "
+                           f"{filename}: {rsp.error}")
+                return False
         return True
 
     def cmd_program_buffer(self, physical_partition_number, start_sector, wfdata, display=True):
@@ -590,6 +594,7 @@
                 return False
         else:
             self.error(f"Error:{rsp.error}")
+            return False
         return True
 
     def cmd_erase(self, physical_partition_number, start_sector, num_partition_sectors, display=True):

Then let qfil act on it:

--- a/edlclient/Library/firehose_client.py
+++ b/edlclient/Library/firehose_client.py
@@ -959,7 +959,12 @@
                                 self.info(f"[qfil] programming {filename} to partition({partition_number})" +
                                           f"@sector({start_sector})...")
 
-                                self.firehose.cmd_program(int(partition_number), int(start_sector), filename)
+                                if not self.firehose.cmd_program(int(partition_number),
+                                                                 int(start_sector), filename):
+                                    self.error(f"Failed to program {filename} to "
+                                               f"partition({partition_number})"
+                                               f"@sector({start_sector})")
+                                    success = False
                 else:
                     self.warning(f"File : {filename} not found.")
                     success = False

Whether a failed write should abort the run or continue and report at the end is a policy call, but
either is better than the current behaviour of reporting raw programming ok. and exiting 0.

Checking the patch phase's xmlsend() result would be worth doing for the same reason.

Why this matters

A flashing tool's core guarantee is that "no error" means "the bytes are on the device". Here a
partition table — including the one describing userdata — can silently not be written, and the tool
still exits 0. Anyone flashing a device in this state has no way to know from edl's output that
they need to verify by reading back.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions