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.
Environment
edl-git 3.52.1.r428.g51e1102-1, commit51e1102)usblib.py), VID:PID05c6:9008prod_name=HN8T174EJKX075edl --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 andstill returns
True.qfilhas no way to know, prints[qfil] raw programming ok., exits0, andthe 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-534There is no
elseforif rsp.resp:, and no error is logged on that path.cmd_program_buffer()(firehose.py:536-593) has the same shape. It does logself.error(f"Error:{rsp.error}")in itselse, but then still falls through toreturn Trueonline 593.
qfil(firehose_client.py:962) calls it and discards the result entirely: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 likethis — one 0.0% line, then straight on to the next file, no error:
versus a successful one:
Because progress lines are written with
\r, the 0.0% line is usually overwritten on a terminal andthe user sees nothing at all.
Evidence
Which writes landed, determined by reading sectors back off the device with
rsand comparing thepartition arrays against the source
gpt_main*.bin/gpt_backup*.bin:gpt_main0.bin→ LUN0 sector 0gpt_backup0.bin→ LUN0 sector 59293691gpt_main2.bin→ LUN2 sector 0gpt_backup2.bin→ LUN2 sector 5115gpt_main3.bin→ LUN3 sector 0gpt_backup3.bin→ LUN3 sector 8187gpt_main5.bin→ LUN5 sector 0gpt_backup5.bin→ LUN5 sector 8187qfilfinished with[qfil] raw programming ok.,[qfil] patching ok, and exit status0.The dropped GPT writes left the device with primary and backup tables that disagreed: same partition
names and LBA ranges, but different
unique_guidon every entry, differenttype_guidon 58 of 77entries on LUN4, and different A/B slot attribute bytes (e.g.
abl_aprimary0x1000000000000000vs backup0x10c0000000000000). Both tables were internally CRC-valid, so noconsistency check on the device would flag it.
Related: the qfil patch phase also ignores responses
Library/firehose_client.py:974-988I hit this too. After writing a fresh
gpt_main0.bin, the patch that fixes up the last partition'ssize —
— did not take effect, with no diagnostic. The result was a structurally valid GPT (both CRCs
correct) in which
userdatahadfirst_lba=6419880, last_lba=6419879, i.e. a zero-lengthpartition. 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
51e1102and compiles:Then let
qfilact on it: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 exiting0.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 toolstill exits
0. Anyone flashing a device in this state has no way to know from edl's output thatthey need to verify by reading back.