Skip to content

ota: bounds check on /api/flash only covers the first chunk, and runs after init_ota - #2189

Open
DrDoof wants to merge 2 commits into
openshwprojects:mainfrom
DrDoof:ota-validate-before-init
Open

ota: bounds check on /api/flash only covers the first chunk, and runs after init_ota#2189
DrDoof wants to merge 2 commits into
openshwprojects:mainfrom
DrDoof:ota-validate-before-init

Conversation

@DrDoof

@DrDoof DrDoof commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

http_rest_post_flash in the Beken HAL has a bounds check that doesn't cover what
actually gets written, and it runs too late to be safe when it does reject something.

The check is this:

init_ota(startaddr);

if (request->contentLength >= 0) {
    towrite = request->contentLength;
}

if (writelen < 0 || (startaddr + writelen > maxaddr)) {
    return http_rest_error(request, -20, "writelen < 0 or end > 0x200000");
}

writelen is request->bodylen, which is only the part of the body that arrived
along with the headers. The loop underneath writes towrite bytes, which comes from
Content-Length, pulling the rest off the socket with recv and bumping startaddr
each time round. It never checks again, and nothing below it does either:
add_otadata walks addr forward per sector and store_sector goes straight to
CMD_FLASH_ERASE_SECTOR followed by flash_write. So a POST to /api/flash/<addr>
with a large Content-Length and a small first chunk passes the check and then erases
its way past maxaddr. That endpoint takes the start address from the URL and only
requires it to be at or above the OTA partition.

The second half is the ordering. init_ota clears flash write protection and mallocs
the sector buffer before the check happens, and the error path returns without
close_ota(), which is the only place that frees the buffer and puts protection back.
init_ota also refuses to run while sector is non-null, so one rejected upload
leaves write protection off and OTA unusable until the device reboots.

I moved the check above init_ota and pointed it at towrite. Nothing else in the
function changed. I left the -20 error code alone in case anything depends on it, but
reworded the message, since it named a constant that isn't what's being compared.

Only the Beken HAL has this check at all. The other HALs take maxaddr and ignore it,
which is worth a separate look, but I didn't want to touch platforms I can't test.

Tested on BK7252N.

…_ota

Two things went wrong here, both reachable from one POST to /api/flash/<addr>.

The bounds check tested writelen, which is only the part of the body that
arrived with the headers, usually well under a kilobyte. The loop underneath
then writes towrite bytes, which comes from Content-Length, advancing startaddr
as it goes and never checking again. A large Content-Length with a small first
chunk therefore walks straight past maxaddr. Check towrite, since that is what
actually gets written.

The check also ran after init_ota, which clears flash write protection and
mallocs the sector buffer. Bailing out at that point returns without
close_ota(), so the buffer is never freed and protection is never restored.
init_ota refuses to start while sector is non-null, so one rejected upload
leaves OTA broken until the next reboot. Validate first, then init.
@DrDoof

DrDoof commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@openshwprojects @DeDaMrAzR @NonPIayerCharacter @divadiow flagging this one since
it's had no eyes yet and it's the one I'd least like to get wrong.

Short version: the bounds check in http_rest_post_flash tests the first chunk of
the body rather than the full Content-Length, and it runs after init_ota has
already dropped flash write protection. So an oversized upload walks past
maxaddr, and a rejected one leaves protection off with the sector buffer still
allocated, which blocks OTA until reboot.

Only the Beken HAL has the check at all, the others take maxaddr and ignore it,
so I've kept the patch to the one file. Tabs throughout.

@divadiow

divadiow commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

**HARDEN RANGE CHECK, THEN MERGE** | Code reviewed | The total-length and validation-order fix is correct. Replace signed startaddr + towrite arithmetic with an overflow-safe subtraction or wide-integer check, and reject zero or negative lengths before init_ota.

startaddr + towrite is signed arithmetic, so a large content length can
wrap it negative and the sum then compares below maxaddr, passing the
check it was meant to fail. Compare with a subtraction instead, with the
operands guarded first so maxaddr - startaddr cannot go negative, and
reject a zero length in the same condition so an empty post no longer
reaches init_ota.
@DrDoof

DrDoof commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Good catch on the overflow, you're right that the signed sum can wrap and then sail straight past the check it was supposed to trip.

Reworked it as a subtraction. The operands get guarded first, so maxaddr - startaddr can't go negative before the comparison happens, and the zero length now falls into the same condition, so an empty POST no longer gets as far as init_ota erasing anything.

Went with the subtraction rather than a 64-bit compare mostly to keep the arithmetic 32-bit on these parts, but either would do the job.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants