Skip to content

Check the received length in the BLE write handlers - #2486

Open
DustinHab wants to merge 4 commits into
InfiniTimeOrg:mainfrom
DustinHab:fix-ble-input-validation
Open

DustinHab wants to merge 4 commits into
InfiniTimeOrg:mainfrom
DustinHab:fix-ble-input-validation

Conversation

@DustinHab

Copy link
Copy Markdown

Fixes #2485.

The BLE write handlers in FSService, DfuService and SimpleWeatherService cast their header onto om_data and then trust the length fields inside it. AlertNotificationService::OnAlert already measures everything against OS_MBUF_PKTLEN, so this follows the same idea and checks against the length that actually arrived before anything is read.

One commit per service so they can be taken separately.

What changes:

  • MOVE no longer terminates the old path in place, which was a write into the received packet at an offset the peer chose. Both paths are copied out instead.
  • Path buffers are fixed maxpathlen arrays rather than arrays sized from the announced length, and the read chunk is capped at 200 bytes. A chunk larger than that never fit into one notification anyway, so nothing that used to work stops working.
  • WRITE_DATA clamps dataSize to the bytes that arrived, instead of handing it to FileWrite as given.
  • plen > maxpathlen becomes a check that leaves room for the terminator, so plen == 256 no longer writes filepath[256] into fileSize.
  • The DFU init parser skips the softdevice list rather than copying it into a stack array sized by the peer, and checks that the CRC is actually inside the packet.
  • nbPacketsToNotify is checked before the modulo that uses it as a divisor.
  • The weather parser checks for the bytes it reads, and the forecast log loop runs to nbDays instead of always five.
  • prepareReadDataResp is removed. Nothing calls it, and it read a file into the flexible array member of a ReadResponse. Say the word if you would rather keep it.
  • MOVE was missing its break and fell through to default.

Testing:

  • Firmware builds clean, and comes out 48 bytes smaller than main. The variable length arrays and the unused function cost more than the checks do.
  • The boundary behaviour of the new path check was exercised on the host under asan and ubsan with path lengths 255, 256 and 65535, a packet one byte short of the announced path, an empty path, and an exact fit.
  • Not exercised against a companion app on hardware. If someone can run a Gadgetbridge file transfer against this branch before it goes in, that would be worth having, since the read chunk cap is the one change a client could notice.

Every command casts its header onto om_data and then uses length fields out
of it without looking at how much data actually arrived.

MOVE terminated the old path in place with header->pathstr[plen] = 0, where
plen is a uint16 from the packet, so the write landed at an offset the peer
picked. It now copies both paths out instead.

DELETE, MKDIR, LISTDIR and MOVE sized a stack array from the announced path
length, and READ sized one from the requested chunk size. On a part with
64 KB of RAM that let a peer ask for far more stack than exists. Paths now
go into fixed maxpathlen buffers and chunks are capped at what a single
notification can carry anyway.

WRITE_DATA passed header->dataSize straight to FileWrite, so a short packet
with a large dataSize wrote memory past the packet into the file. It is now
clamped to what arrived.

READ and WRITE rejected a path only when plen > maxpathlen, so plen == 256
got through and filepath[256] wrote into the next member, fileSize. The
comment on that line said "counts for null term", so >= was the intent.

MOVE also fell through to default because its break was missing.

prepareReadDataResp is removed. Nothing calls it, and it read a file
straight into the flexible array member of a ReadResponse the caller would
have had on the stack.

Boundary cases of the new check were exercised on the host under asan and
ubsan: path length 255 and 256, packet one byte short, empty path, and 65535.
The init packet parser read a softdevice count out of the packet and sized a
stack array from it, up to 128 KB on a part with 64 KB of RAM, then walked
past the end of the buffer to find the CRC behind it. Only the CRC is used,
so the list is skipped rather than copied, and both the fixed part and the
CRC offset are checked against the length that arrived.

The start packet handler read twelve bytes and the control point read one or
two without checking for them.

nbPacketsToNotify is zero until the peer asks for packet receipt
notifications. A peer that skips that request and sends data reached
nbPacketReceived % nbPacketsToNotify with a zero divisor.
CreateCurrentWeather reads up to offset 52 and copies 32 bytes from offset 16
into the location string, and CreateForecast reads up to offset 35, none of
it checked against the packet. A short write left whatever followed the
buffer in the city name shown on the watch face. This service has no
DfuAndFsMode gate in front of it.

The forecast log loop also went over all five days regardless of how many
were present, dereferencing empty optionals in a debug build.
Every branch above guards against characteristic being null, the final else
did not. Only reachable in a debug build, since the dereference is inside
NRF_LOG_INFO.
@github-actions

Copy link
Copy Markdown

Build size and comparison to main:

Section Size Difference
text 387824B -160B
data 936B 0B
bss 22936B 0B

Run in InfiniEmu

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.

BLE handlers use length fields from the packet without checking how much data arrived

1 participant