Skip to content

fix: zero-init struct tm before gmtime_r; replace strcpy with memcpy#2753

Open
stark256-spec wants to merge 1 commit into
nasa:devfrom
stark256-spec:fix/time-gmtime-uninitialized-and-strcpy
Open

fix: zero-init struct tm before gmtime_r; replace strcpy with memcpy#2753
stark256-spec wants to merge 1 commit into
nasa:devfrom
stark256-spec:fix/time-gmtime-uninitialized-and-strcpy

Conversation

@stark256-spec

Copy link
Copy Markdown

Fixes #2735 and #2737.

#2735gmtime_r() returns NULL for out-of-range time_t values, leaving struct tm uninitialised. strftime() on uninitialised memory is UB. Zero-initialise tm with memset() before calling gmtime_r() so a failed conversion produces a deterministic epoch-like string.

#2737 — Both strcpy sites in cfe_assert_init.c and cfe_tbl_internal.c append a fixed-length literal into a buffer where space was already verified. Replace with memcpy(dst, literal, sizeof(literal)) to copy exactly the required bytes including the NUL terminator.

Two independent static-analysis findings addressed in one commit since
they touch disjoint files and carry the same risk category.

1. cfe_time_api.c — CFE_TIME_Print (fixes nasa#2735)
   gmtime_r() returns NULL when the input time_t is outside the range
   representable by struct tm (overflow or platform-specific limits).
   The subsequent strftime() then reads from an uninitialised struct,
   producing undefined behaviour. Zero-initialise tm with memset before
   calling gmtime_r() so that a failed conversion yields a deterministic
   epoch-like formatted string rather than garbage or a crash.

2. cfe_assert_init.c + cfe_tbl_internal.c — strcpy (fixes nasa#2737)
   Both sites append a known, fixed-length literal ('.tmp' and '(*)')
   into a buffer where available space has already been verified by the
   surrounding bounds check. Replace strcpy with memcpy(dst, literal,
   sizeof(literal)) which copies exactly the required bytes including the
   NUL terminator without relying on runtime null-termination scanning.
@dzbaker

dzbaker commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Thank you @stark256-spec for this contribution! Can you provide some more context behind this solution and steps you followed to verify it?

@stark256-spec

Copy link
Copy Markdown
Author

Sure, happy to give more detail.

Context for the changes:

All three are static-analysis findings (the "uninitialized read" / "unbounded strcpy" kind that show up in Coverity/cppcheck-style scans):

  • cfe_time_api.c: if gmtime_r() ever returned NULL, tm would be left uninitialized and then read by strftime(). Added memset(&tm, 0, sizeof(tm)) right before the call so tm is always in a defined state regardless of what gmtime_r() does.
  • cfe_assert_init.c / cfe_tbl_internal.c: both were flagged for strcpy() of a string literal into a buffer whose remaining length had already been computed by the surrounding code. Swapped to memcpy(..., sizeof("literal")), which copies the exact same bytes (including the NUL, since sizeof on a string literal includes it) but gives the analyzer a compile-time-constant size to check against the destination instead of trusting strcpy's "copy until NUL" behavior.

How I verified it:

Built this branch with unit tests enabled (ENABLE_UNIT_TESTS=TRUE -DSIMULATION=native) and ran the existing coverage suites for the two FSW modules touched:

  • modules/time/ut-coverage → 481 total / 464 pass / 0 fail (17 N/A). Test_Print (9/9) exercises CFE_TIME_Print() across several time_t values, including Seconds = 0x7fffffff, and asserts the exact formatted string each time - confirms the memset doesn't change output for any value cFE can actually produce.
  • modules/tbl/ut-coverage → 1358 total / 1358 pass / 0 fail. Test_CFE_TBL_GetInfo covers CFE_TBL_MarkNameAsModified() end-to-end via CFE_TBL_Modified()/CFE_TBL_GetInfo(), including the boundary case where LastFileLoaded is filled to capacity and the trailing "(*)" marker (4 bytes incl. NUL, same size as the new memcpy) gets truncated in - that assertion passes too.

cfe_assert itself doesn't have a ut-coverage harness (it's the on-target test framework, not something that gets unit tested), so that one doesn't have an automated test either way - it's a same-bytes strcpy→memcpy swap into a buffer whose space was already bounds-checked by NameLen just above it.

On the gmtime_r path specifically - I didn't add a new test for the "gmtime_r returns NULL" branch itself, on purpose. CFE_TIME_SysTime_t.Seconds is a uint32, so after adding the mission epoch offset, sec tops out around 4.6e9 (~year 2116). gmtime_r only returns NULL once tm_year would overflow int, which needs a time_t around 6.8e16 - about 7 orders of magnitude beyond anything this function can ever be called with. Forcing that branch in a test would mean --wrap-ing gmtime_r at link time, which felt like a lot of new test machinery for a one-line defensive zero-init whose only job is to make the analyzer happy. Happy to add it if you'd still like to see it though.

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.

Uninitialized Variable in cfe_time_api.c

2 participants