Skip to content

Reject DataFlash FMT records whose length and format disagree - #15086

Open
nicholasaleks wants to merge 1 commit into
mavlink:masterfrom
nicholasaleks:fix/qgc-04-dataflash-fmt-validation
Open

nicholasaleks wants to merge 1 commit into
mavlink:masterfrom
nicholasaleks:fix/qgc-04-dataflash-fmt-validation

Conversation

@nicholasaleks

Copy link
Copy Markdown

Description

Related to: https://github.com/nicholasaleks/infected-drones/tree/main/QGC-04_dataflash_bin_parser_oob

Every FMT record in a .bin log carries both a length and a format string, and the parser trusts each of them separately without ever comparing the two.

length is a uint8_t read from the log. Both parser passes compute "fmt.length - 3" to get a payload size, and integer promotion makes that a negative int for length < 3. The bounds check in iterateMessages(), "pos + payloadSize > size", is meant to catch an over-long payload; a negative value makes the left side smaller, so it never fires. The cursor then moves backward by that amount. Both loops consume the 3-byte header with pos += 3 first, so length == 0 is exactly net zero movement and the same record is re-parsed forever. That hangs parseFmtMessages() during the initial format scan, before a single message is iterated: opening such a log pins a core at 100% with no error reported.

Independently, parseMessage() walks the format string and accumulates an offset from formatCharSize() alone, up to 64 bytes for 'a' or 'Z'. It is not given the payload length and has nothing to check against, so a format declaring more bytes than the record holds reads past the end of it. The buffer is a memory-mapped file, so that read either lands in the zero-filled tail of the last page or faults.

Both follow from the same thing: the two sizes are never reconciled. They are first known together at registration, so check them there. A format is registered only when length leaves room for a header and the fields the format string declares fit inside the remaining payload. That keeps "length - 3" non-negative everywhere it is used and bounds the offset parseMessage() accumulates, without changing its signature or its call sites.

Adds a test covering length 0, 1 and 2, a format wider than the declared payload, and a consistent record that must still be accepted.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/Build changes
  • Other

Testing

  • Tested locally
  • Added/updated unit tests
  • Tested with simulator (SITL)
  • Tested with hardware

Platforms Tested

  • Linux
  • Windows
  • macOS
  • Android
  • iOS

Flight Stacks Tested

  • PX4
  • ArduPilot

Screenshots

Video in https://github.com/nicholasaleks/infected-drones/tree/main/QGC-04_dataflash_bin_parser_oob

Checklist

  • I have read the Contribution Guidelines
  • I have read the Code of Conduct
  • My code follows the project's coding standards
  • I have added tests that prove my fix/feature works
  • New and existing unit tests pass locally

Related Issues

https://github.com/nicholasaleks/infected-drones/tree/main/QGC-04_dataflash_bin_parser_oob


By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).

Every FMT record in a .bin log carries both a length and a format string, and
the parser trusts each of them separately without ever comparing the two.

length is a uint8_t read from the log. Both parser passes compute
"fmt.length - 3" to get a payload size, and integer promotion makes that a
negative int for length < 3. The bounds check in iterateMessages(),
"pos + payloadSize > size", is meant to catch an over-long payload; a negative
value makes the left side smaller, so it never fires. The cursor then moves
backward by that amount. Both loops consume the 3-byte header with pos += 3
first, so length == 0 is exactly net zero movement and the same record is
re-parsed forever. That hangs parseFmtMessages() during the initial format scan,
before a single message is iterated: opening such a log pins a core at 100% with
no error reported.

Independently, parseMessage() walks the format string and accumulates an offset
from formatCharSize() alone, up to 64 bytes for 'a' or 'Z'. It is not given the
payload length and has nothing to check against, so a format declaring more
bytes than the record holds reads past the end of it. The buffer is a
memory-mapped file, so that read either lands in the zero-filled tail of the
last page or faults.

Both follow from the same thing: the two sizes are never reconciled. They are
first known together at registration, so check them there. A format is
registered only when length leaves room for a header and the fields the format
string declares fit inside the remaining payload. That keeps "length - 3"
non-negative everywhere it is used and bounds the offset parseMessage()
accumulates, without changing its signature or its call sites.

Adds a test covering length 0, 1 and 2, a format wider than the declared
payload, and a consistent record that must still be accepted.
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Build results unavailable — artifact download from one or more platform workflows failed
(likely artifact retention expiry or transient API error). The combined report cannot be generated for this run.

See the Build Results workflow run for details.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant