Skip to content

mm_load.c: validate DataSize locally in MM_PokeMem before dispatch#116

Open
nurdymuny wants to merge 6 commits into
nasa:mainfrom
nurdymuny:scj-hardening-pokemem
Open

mm_load.c: validate DataSize locally in MM_PokeMem before dispatch#116
nurdymuny wants to merge 6 commits into
nasa:mainfrom
nurdymuny:scj-hardening-pokemem

Conversation

@nurdymuny

Copy link
Copy Markdown

Summary

MM_PokeMem() in fsw/src/mm_load.c switches on
CmdPtr->Payload.DataSize to choose between
CFE_PSP_MemWrite8/16/32(). The existing comment above the empty
default: case explicitly documents the upstream-validation
reliance:

/*
** We don't need a default case, a bad DataSize will get caught
** in the MM_VerifyPeekPokeParams function and we won't get here
*/
default:
    break;

This is correct under the current MM_PokeCmd() dispatch path, which
always calls MM_VerifyPeekPokeParams() before MM_PokeMem(), but
the local function is the one that actually issues the PSP write
calls and should not depend on a cross-translation-unit invariant
for correctness.

Failure mode

Any future code path that reaches MM_PokeMem() without going
through MM_VerifyPeekPokeParams() — a refactor of MM_PokeCmd
dispatch, a new ground-command entry that delegates to
MM_PokeMem, a test harness, or an EDS-generated wrapper — would
drop straight into the switch with PSP_Status left at its
initialised value of CFE_PSP_ERROR_NOT_IMPLEMENTED and the
command counter unchanged but no diagnostic event sent. The failure
would be silent.

Fix

Add an explicit DataSize check at function entry. On the new error
path, raise MM_DATA_SIZE_BITS_ERR_EID (same EID
MM_VerifyPeekPokeParams() uses for the same condition) and
return CFE_PSP_ERROR so the caller's command-counter machinery
behaves correctly. No behavior change on valid DataSize values.

How I found this

Found by the Davis Manifold geometric vulnerability detector
(`scj-hunt`) while ranking MM functions by attacker-input-handling
density. MM_PokeMem ranked top of the cFS MM bundle under the
psp_memwrite_unguarded compound rule.

Relationship to nasa/fprime#5262

This is the same brick-wall hardening template as
nasa/fprime#5262, which
M. Starch merged earlier today for FileUplink::File::open. Both
PRs address the same shape: a local function whose memory-safety
correctness relies entirely on an upstream type narrowing or
upstream verifier in a different translation unit, with no
diagnostic on the silent-failure path if the upstream invariant is
ever broken.

Future work

The same defensive pattern applies to MM_PokeEeprom,
MM_LoadMem{8,16,32}FromFile, MM_DumpMem{8,16,32}ToFile, and the
MM_FillMem* family. Happy to scope a follow-up sweep covering
those once this template is accepted.

Test plan

  • No behaviour change on valid DataSize
    (BYTE_BIT_WIDTH / WORD_BIT_WIDTH / DWORD_BIT_WIDTH).
  • On invalid DataSize, function now returns CFE_PSP_ERROR
    and raises MM_DATA_SIZE_BITS_ERR_EID instead of returning the
    initialised CFE_PSP_ERROR_NOT_IMPLEMENTED with no event.
  • Existing unit tests in unit-test/mm_load_tests.c should
    continue to pass.
  • A unit test exercising the new error path would be valuable; I am
    happy to add one if reviewers can advise whether to put it under
    MM_PokeMem_Test_* in mm_load_tests.c or as a stand-alone
    fault-injection case.

jphickey and others added 6 commits May 13, 2026 09:10
Reset MISSION_REV to 0xFF and build number to 1.
…use-updated-static-analysis-workflow

Part cFS/workflows#129, Use Updated Static Analysis Workflow
MM_PokeMem() switches on CmdPtr->Payload.DataSize to choose between
CFE_PSP_MemWrite8(), CFE_PSP_MemWrite16(), and CFE_PSP_MemWrite32().
The function relies on MM_VerifyPeekPokeParams() having already
rejected any DataSize value that is not one of
MM_INTERNAL_BYTE_BIT_WIDTH, MM_INTERNAL_WORD_BIT_WIDTH, or
MM_INTERNAL_DWORD_BIT_WIDTH, as the comment above the empty default
case explicitly states.

This is correct under the current MM_PokeCmd dispatch path, which
always calls MM_VerifyPeekPokeParams before MM_PokeMem, but the
local function is the one that actually issues the PSP write calls
and should not depend on a cross-translation-unit invariant for
correctness.  Any future code path that reaches MM_PokeMem without
going through MM_VerifyPeekPokeParams --- a refactor of MM_PokeCmd
dispatch, a new ground-command entry that delegates to MM_PokeMem,
a test harness, or an EDS-generated wrapper --- would drop straight
into the switch with PSP_Status left at its initialised value of
CFE_PSP_ERROR_NOT_IMPLEMENTED and the command counter unchanged but
no diagnostic event sent.  The failure would be silent.

Add an explicit DataSize check at function entry, returning
CFE_PSP_ERROR and raising MM_DATA_SIZE_BITS_ERR_EID on the new
error path.  Matches the defensive style MM_VerifyPeekPokeParams
already uses for the same field, and the defense-in-depth template
nasa/fprime PR 5262 used for the same kind of brick-wall reliance
on an upstream type narrowing.

Found by the Davis Manifold geometric vulnerability detector
(scj-hunt) while ranking MM functions by attacker-input-handling
density.  MM_PokeMem ranked top of the MM bundle under the
psp_memwrite_unguarded compound rule.

The same defensive pattern applies to MM_PokeEeprom,
MM_LoadMem variants, MM_DumpMem variants, and the MM_FillMem
family.  Happy to scope a follow-up sweep covering those once
this template is accepted.

Signed-off-by: Bee Rosa Davis <bee_davis@alumni.brown.edu>
Apply the same defense-in-depth template to MM_PokeEeprom() that
the preceding commit applied to MM_PokeMem().

MM_PokeEeprom() switches on CmdPtr->Payload.DataSize to choose
between CFE_PSP_EepromWrite8(), CFE_PSP_EepromWrite16(), and
CFE_PSP_EepromWrite32(), and relies on MM_VerifyPeekPokeParams()
having already rejected any invalid DataSize value, as the
identical brick-wall comment block above the empty default case
explicitly states.

This is correct under the current MM_PokeCmd dispatch path but
the local function is the one that actually issues the PSP
EEPROM write calls, and EEPROM writes are particularly sensitive
because they persist across power cycles.  A silent failure
here --- silent because PSP_Status is left at its initialised
value CFE_PSP_ERROR_NOT_IMPLEMENTED and no diagnostic event is
sent --- could leave the spacecraft in an unintended persistent
state across reboots, with the perf-log entry/exit pair
unbalanced relative to the actual work performed.

Add the same explicit DataSize check at function entry, returning
CFE_PSP_ERROR and raising MM_DATA_SIZE_BITS_ERR_EID on the new
error path.  Matches the MM_PokeMem hardening template added in
the preceding commit on this PR.

Found by scj-hunt's ATTEND brain primitive (v0.15 Phase A1)
during a patch-twin sweep with MM_PokeMem as the seed:

  $ scj-hunt twins --bundle cfs_apps_mm_v01 --of MM_PokeMem
  rank 12: MM_PokeEeprom    score 5.5

The patch-twin discovery exposes a gap in the Sudoku Principle
catalog: the `psp_memwrite_unguarded` compound rule that flagged
MM_PokeMem at score 10.0 keys off CFE_PSP_MemWrite{8,16,32} sinks
and does not include CFE_PSP_EepromWrite{8,16,32}.  ATTEND
clustered the two functions together anyway because they share
the same structural shape (3-way switch on DataSize, brick-wall
reliance on MM_VerifyPeekPokeParams, identical doc-comment
above default case) --- exactly the geometric patch-twin signal
the brain primitive is designed to surface.

The follow-up sweep on MM_LoadMem{8,16,32}FromFile,
MM_DumpMemToFile, and the MM_FillMem family is still open;
ATTEND ranks MM_LoadMemWIDCmd (the WID dispatcher for the
LoadMem family) at rank 3 above this candidate.

Signed-off-by: Bee Rosa Davis <bee_davis@alumni.brown.edu>
@nurdymuny

Copy link
Copy Markdown
Author

Extended this PR with a second commit (4eeced8) applying the identical hardening template to MM_PokeEeprom().

Identical bug shape: 3-way switch on CmdPtr->Payload.DataSize, brick-wall reliance on MM_VerifyPeekPokeParams(), same default: break; with the same doc-comment about the upstream validator. EEPROM writes are particularly worth hardening because they persist across power cycles — a silent failure from a cross-translation-unit invariant breaking would leave the spacecraft in an unintended persistent state across reboots.

Methodology note: MM_PokeEeprom wasn't surfaced by the original psp_memwrite_unguarded compound rule (which keys off CFE_PSP_MemWrite{8,16,32} sinks, not the EEPROM variants CFE_PSP_EepromWrite{8,16,32} — a gap in the catalog). It was surfaced by a patch-twin query using the new gigi brain primitive ATTEND:

$ scj-hunt twins --bundle cfs_apps_mm_v01 --of MM_PokeMem
rank 12: MM_PokeEeprom    score 5.5

ATTEND clustered the two functions together based on their structural similarity (same dispatch shape, same brick-wall pattern, same doc-comment) even though the MM_PokeMem rule didn't fire on MM_PokeEeprom. This is the patch-twin discovery the Davis Manifold framework's WISH / ATTEND verbs are designed for — finding sibling-shaped code the keyword-based rules missed.

Happy to continue the sweep on MM_LoadMem{8,16,32}FromFile, MM_DumpMemToFile, and the MM_FillMem family if the same template applied to the EEPROM case lands cleanly — ATTEND ranked MM_LoadMemWIDCmd (the WID dispatcher for the LoadMem family) at rank 3 right above MM_PokeEeprom.

@nurdymuny

Copy link
Copy Markdown
Author

Quick follow-up on the LoadMem / DumpMem / FillMem family sweep I offered to do.

I swept all the remaining family functions and the literal pattern doesn't recurMM_PokeMem and MM_PokeEeprom look like the only two that share the brick-wall shape. Reporting honestly rather than padding the PR:

Function Pattern Already defended?
MM_LoadMemFromFileCmd switch (MMFileHeader.MemType) yes — default: Status = OS_ERROR; at mm_cmds.c:543
MM_FillMemCmd switch (Msg->Payload.MemType) yes — default: Status = CFE_PSP_ERROR; at mm_cmds.c:690
MM_DumpMemToFileCmd switch (MMFileHeader.MemType) yes — default: Status = CFE_PSP_ERROR; at mm_cmds.c:874
MM_LoadMemWIDCmd no switch — MM_VerifyLoadDumpParams + memcpy n/a — different shape
MM_DumpInEventCmd no switch — MM_VerifyLoadDumpParams n/a — different shape
MM_FillMem8 / 16 / 32 type pre-specialized by file (MEM8 / MEM16 / MEM32) n/a — no DataSize axis

So this PR stays at the two original commits. I'd rather report a clean negative than upsize the diff for the sake of it.

If you'd prefer a separate hardening pass on something else in MM that is worth doing — e.g. tightening the MM_VerifyPeekPokeParams family or adding a static_assert that the three MM_INTERNAL_*_BIT_WIDTH constants stay in sync with the switch arms — I'm happy to file that as a fresh PR. Just say the word.

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.

4 participants