Preserve multipart OTA MD5 after Update initialization

Arduino-ESP32 2.0.17 UpdateClass::begin() clears _target_md5 before
initializing the digest builder (Updater.cpp:123-126). The installed pinned
framework package is 3.20017.241212+sha.dcc1105b. Setting the multipart
digest before begin therefore skipped the final comparison in end(),
which only compares a nonempty _target_md5 (Updater.cpp:303-308).

Set the supplied MD5 after Update.begin succeeds. If the digest is rejected,
abort the begun update, release upload admission, and return HTTP 400 before
assigning an owner or extending its receive timeout.

Model the pinned begin-clears-digest behavior in the host fixture using
independent digest vectors. Verify malformed and mismatched digests, actual
byte corruption, disconnect cleanup, and later valid admission. Negative
controls restore the original ordering and remove abort/busy cleanup.

Validation: python3 -B test/test_async_web_lifecycle.py -v passed 4/4,
including C++ ASan/UBSan execution and negative controls. No PlatformIO or
hardware validation was run in this worktree.
This commit is contained in:
mikecarper committed 2026-10-06 14:42:20 -07:00
1 parent be2ab346b0
commit 70bf4bdfeb
3 files changed
+127 -23

No files matched your search

+16
View File
@@ -67,6 +67,22 @@ static std::atomic<bool> ota_upload_busy{false};"""
with self.assertRaises(AssertionError):
self.compile_and_run(old,"-DESP32=1","-DTEST_OTA_BODY_TIMEOUT=1")
def test_md5_survives_begin_and_rejected_digest_releases_update(self):
fixture=self.uploader_fixture()
self.compile_and_run(fixture,"-DESP32=1")
guard=extract_braced(fixture,"if(!Update.setMD5(")
marker=" #if defined(ESP8266)\n int cmd"
before_begin=fixture.replace(guard,"",1).replace(marker,guard+"\n"+marker,1)
self.assertNotEqual(fixture,before_begin,"The negative control must set MD5 before begin")
with self.assertRaises(AssertionError):
self.compile_and_run(before_begin,"-DESP32=1")
for cleanup in ("Update.abort();", "ota_upload_busy.store(false);"):
with self.subTest(rejected_digest_cleanup=cleanup):
missing=fixture.replace(guard,guard.replace(cleanup,"",1),1)
self.assertNotEqual(fixture,missing,"The negative control must remove rejection cleanup")
with self.assertRaises(AssertionError):
self.compile_and_run(missing,"-DESP32=1")
def test_tcp_listener_can_rebind_with_completed_connections(self):
cached=ROOT/".pio/libdeps/heltec_v4_repeater_observer_mqtt/AsyncTCP/src/AsyncTCP.cpp"
source=(cached.read_text() if cached.exists() else urllib.request.urlopen(