Skip to content Skip to footer

NASA cFS version Aquila Software Vulnerability Assessment

Written by: Milenko Starcik, Andrzej Olchawa, Ricardo Fradique, Ayman Boulaich

NASA’s Core Flight System (cFS) is an open-source software framework that supports mission operations by providing a modular and scalable architecture. Despite its widespread use in aerospace applications, a recent security assessment of cFS version Aquila uncovered critical vulnerabilities that could be exploited to disrupt mission-critical systems. These include Remote Code Execution (RCE), Denial of Service (DoS), and Path Traversal vulnerabilities.

Key Findings

The security assessment revealed the following vulnerabilities in cFS and its underlying modules:

  • Denial of Service (DoS) via Invalid Memory Access – Exploitable Memory Management Module (MM) flaws allow unauthorized memory access, leading to application crashes.
  • Path Traversal in OSAL – Improper validation of file paths enables attackers to write files outside designated directories.
  • Remote Code Execution (RCE) via GOT Overwrite – An attacker can manipulate memory structures to achieve arbitrary code execution.
  • Denial of Service (DoS) on External Applications – Inadequate cleanup mechanisms can be abused to fill resource tables, preventing new applications from launching.

Watch the multiple vulnerabilities found in the video down below:

The Core Flight System (cFS) was developed to create a platform—and project-independent, reusable software framework [RD-3]. It includes a Platform Abstraction Layer to ensure portability and Applications that provide essential mission functionality. Additionally, CFS offers compile-time configuration parameters and run-time command/table parameters, enabling flexibility and scalability.

Figure 1 – cFS architecture layers [RD-3]

Its architecture is divided into five main layers, as described in [RD-3] and illustrated in Figure 1:

  • RTOS/Boot – Provides the interface between the processor and the flight software, which can be based on commercial, open-source, or custom software.
  • OS Abstraction Layer (OSAL) – A software library that provides a single Application Programming Interface (API) to the Core Flight Executive (cFE), regardless of the underlying real-time operating system.
  • Platform Support Package (PSP) – A software library that offers a single API to the underlying avionics hardware and board support package.
  • Core Flight Executive (cFE) – A portable, platform-independent framework that creates an application runtime environment by providing services common to most flight applications.
  • Applications – A set of applications that deliver mission functionality, combining cFS community and mission-specific apps.

In addition to these layers, development tools and ground systems are used to test and operate cFS. The selection of ground systems and tools typically varies by project.

This article focuses specifically on the OS Abstraction Layer [RD-4], the Platform Support Package [RD-5], and the Application layer, with a particular emphasis on the Memory Management Application [RD-6].

We learned in this review that the Memory Management Module (MM) does not validate memory addresses correctly before reading from or writing to them in most function calls. These are split between MM_Dump and MM_Load depending on whether they are during reading or writing memory. The MM relies on the Memory ranges specified in the CFE_PSP_MemoryTable to verify all memory operations before their execution, which are first defined in cfe_psp_memory.c through the use of CFE_PSP_MemRangeSet. However, most operating systems’ default configuration announces the entire memory space as valid, hence effectively verifying all operations even though they access invalid areas. While the user must define these ranges at build time, this is poorly documented or omitted from the initial install documentation, with no build-time warning to alert the user to issues.

Further, even if the app is compiled for systems lacking direct access to physical memory (by using the ram_notimpl module of PSP), the MM contains a function that tries to access it directly instead of using the memory manipulation functions of the PSP, resulting in crashes.

Under normal circumstances, the MM_DUMP_MEM_TO_FILE_CC command is responsible for dumping a specified number of bytes from a memory location to a file:

Figure 2 – Command description and documentation

This command should be protected against misuse by validating the source address, the address range, the data size requested, and the file path where it is supposed to be saved. However, we’ve found that not all these checks are correctly performed, allowing for a denial of service by crashing the cFS instance.

Figure 3 – Error conditions that the function should properly validate before execution.

The command structure is defined in mm/fsw/inc/mm_msg.h:

Figure 4 – Type definition for the DumpMemToFile Command Payload.

This structure is used by MM_DumpMemToFileCmd (mm/fsw/src/mm_dump.c) to extract each field before validating the inputs.

The first validation is the symbolic address from which to dump the memory:

Figure 5 – Call to validate symbolic addresses passed to the function.

MM_ResolveSymAddr is defined in mm/fsw/src/mm_utils.c:

Figure 6 – Address validation implementation returns True even if the address is an invalid memory location.

This function will always return Valid = True if an offset is included. At lines 525-526, if the symbol name is a null string, it uses the offset inputted as an absolute address and returns True. Otherwise, it returns the address of the symbol sent as input plus the offset.

It then verifies the parameters using MM_VerifyLoadDumpParams:

Figure 7 – Load and dump operation parameter validation.

This function first validates if the Memory type selected is valid and if the number of bytes requested is lower than the maximum size for that memory type before calling CFE_PSP_MemValidateRange to validate if the address is part of a valid memory range as defined in the PSP memory table:

Figure 8 – PSP memory validation.

The first step of the vulnerability is present at the next point. The PSP table is defined at boot time by CFE_PSP_SetupReservedMemoryMap. For Linux systems, the default configuration defines the entire memory space as valid for read/write operations:

Figure 9 – Default PSP memory table setup.

This is also true for other operating systems, where only mcp750-vxworks defines a limited memory space by default:

Figure 10 – The default configuration for other operating systems is included with the bundle.

Any possible RAM address inputted from MM_DumpMemToFileCmd will pass the validation, and execution will continue.

This memory address is then used to define the file header that will be used to guide the memory dump:

Figure 11 – Inputted memory address used to define the file header.

Figure 12 – Function tests if the file can be opened to read and write before calling DumpMemToFile.

At this stage, the program only verifies if it can open and write into a file, which creates another vulnerability (more on this in section 2.3).

If the memory type selected in the command is MM_RAM or MM_EEPROM, it then calls MM_DumpMemToFile with the previously defined file header. At this point, it uses the offset defined in the file header as the source pointer:

Figure 13 – Implementation of the memory dump function, which uses the address inputted as a source pointer for memcpy.

Which is then directly used in a memcpy operation, triggering a segmentation fault by trying to copy protected memory outside the scope of the process:

Figure 14 – Segfault location: if an invalid source is input, it will crash the cFS instance.

This can be demonstrated by calling the MM_DUMP_MEM_TO_FILE_CC command using cmdUtil [RD-2]:

Figure 15 – Proof of Concept for the invalid memory read is present in Memory Manager.

Upon receiving a request to dump memory from address 0x40, an invalid address, the cSF instance running on the top part of the screen was terminated.

The OS_OpenCreate function, called from MM_DumpMemToFileCmd (Figure 12), does not validate if the destination file path is on a sensitive location, relying only on the OS file permission flags. On the default configuration, the cfe_es_startup.scr file responsible for starting the initial applications is created with permissive flags that allow for read-write access. In addition, MM_DumpMemToFileCmd starts writing the file header into the file inputted before attempting to dump the memory contents into it. If this file exists, it is overwritten at this point. This allows for the corruption of sensitive files by using them as target paths for the MM_DumpMemToFileCmd function, as demonstrated in Figure 17 and Figure 18:

Figure 16 – cfe_es_startup.scr file responsible for starting all applications in order before corruption.

Figure 17  – The cfe_es_startup.scr file is overwritten and corrupted before the program crashes due to a segfault.

Figure 18 – The application no longer responds to commands due to a corrupted startup file.

The impact of this vulnerability can be further increased when combined with the path traversal finding demonstrated in 2.3.

The same vulnerability is present in the MM_DumpInEventCmd. The goal of this command is to dump a user-defined number of bytes directly into an event sent by telemetry.

This method uses the same validation methods as above, calling MM_FillDumpInEventBuffer instead of MM_DumpMemToFile. It also uses the source address directly in a memcpy call, again triggering a segmentation fault by accessing invalid memory.

Figure 19 – Call memcpy using the user-inputted memory address.

This can be demonstrated in the same way as before using cmdUtil:

Figure 20 – Segmentation fault triggered by reading from an invalid address using MM_DumpInEventCmd.

Similar flaws regarding invalid memory writes are also present in some methods from MM_Load.

The MM_FillMemCmd is designed to allow the user to fill a memory block directly with a user-defined pattern. It starts by validating its inputs in the same way as the previous commands before calling MM_FillMem:

Figure 21 – MM_FillMemCmd validation and execution.

MM_FillMem sets up a temporary buffer to prepare the data for writing and then calls memcpy with the user-inputted target address.

Figure 22 – Memory is filled by memcpy between a temporary buffer and the user-inputted memory address.

This can cause a denial of service by crashing the program due to invalid memory access, as demonstrated below:

Figure 23 – MM_FillMemCmd segfault by writing to an invalid address.

This command is similar to MM_FillMemCmd in that it allows the user to load data directly into memory by reading the contents of a file. It starts by reading the file header, verifying that the file size matches the expected size, and validating the data integrity by checking the checksum of the contents and comparing it with the checksum in the file.

Figure 24 – File read, parameter validation, and data integrity check.

It then proceeds to validate the inputs and memory ranges using the same methods as described above before calling MM_LoadMemFromFile:

Figure 25  – Call to load the memory from the specified file.

The destination address DestAddress is defined from the file header and can be user controlled. This is then used in a memcpy operation:

Figure 26 – memcpy operation that writes into memory the file contents of the file passed as argument.

This is demonstrated below by calling MM_LoadMemFromFileCmd to load from a previously created but corrupt memory dump file:

Figure 27 – Segmentation fault caused by loading a corrupted memory dump file into memory.

The same vulnerability can be triggered from MM_LoadMemWIDCmd. This command follows the same validation logic as above before manipulating memory using memcpy.

Figure 28 – memcpy operation that writes into memory the file contents of the array sent as an argument in the command.

This is demonstrated below, showing the ability to crash cFS by writing into invalid memory:

Figure 29 – Segmentation fault caused by trying to write the inputted array into an invalid memory location.

Recommendation: Ensure memory access is validated before accessing user-defined locations. The default configuration should not mark the entire memory space as valid without warning. A consistent memory access framework should be used through PSP or self-handled checks to avoid conflicting methods. It is also essential to populate the PSP memory table with each application’s memory range during initialization, limiting the impact of the vulnerability. Limiting memory read/write operations to valid symbols, validated at runtime, helps reduce the attack surface. Additionally, write access to sensitive files (such as configuration and initialization files) should be restricted to prevent file corruption that could compromise the application’s functionality

OSAL acts as an abstraction layer between cFS and the underlying operating system. Upon receiving a call to open a file using OS_OpenCreate, OSAL tries to translate the path between the application’s virtual path and the host’s real path (Figure 30).

Figure 30 – OSAL translates virtual paths to local paths using OS_TranslatePath.

This function verifies if the path starts with a ‘/’ character and if the file name is shorter than the maximum file name length allowed before using OS_FileSys_FindVirtMountPoint to validate if the file is within the virtual mount point.

Figure 31 – First path and filename validations.

However, this function only verifies if the user-inputted file path begins with the exact string as the application’s virtual mount point (lines 106-107, Figure 32). Because this isn’t necessarily the absolute path file path for a given file, it allows for a path traversal on any user-inputted file path. 

Figure 32 – File path validation.

This is demonstrated below using MM_DumpMemToFileCmd, which uses OS_OpenCreate to write a memory dump into a file.

For demonstration purposes, we can use gdb to show that cFS was using /cf as a virtual mount point in our example:

Figure 33 – Virtual path in use by the binary.

Before calling the command, we can verify that the run folder only contains the binary, the EEPROM data, and the cf folder that’s mounted as a virtual mount point:

Figure 34 – Parent folder of the virtual mount point before the file creation operation.

Then calling the command, using /cf/../dd as destination path:

Figure 35  – Using MM_DumpMemToFileCmd to create a new file on a folder outside the virtual mount point.

We were able to create a new file outside the virtual mount point:

Figure 36 – Parent folder of the virtual mount point after the file creation operation.

This file path traversal can thus be used to write into any location in the system where the application user has permission.

Recommendations: File path validation is performed on the user-inputted string, not the absolute path to a file. To avoid issues with relative paths being included in the string, it is recommended that all file paths be converted into absolute paths before validation. The real path () function, present in stdlib, can derive absolute path names from relative paths, which would help mitigate this vulnerability.

Further exploring the Memory Management (MM) in cFS reveals that the module’s functionalities are very useful and powerful. However, the module imposes very few restrictions on managing memory. Expanding on the research from the section 2.2 of this report, we have found a way to abuse MM functionality and gain an RCE in cFS.

In its default configuration, OSAL is built with the ability to run shell commands disabled:

Figure 37 – Configuration setting to disable the shell API at build time.

However, since libc is still included, the system function is included by default, even if not directly called anywhere in the binary. This function calls the execlp function with an input string as an argument.

For context, a dynamically linked ELF binary uses a look-up table called the Global Offset Table (GOT) to resolve functions located in shared libraries, like libc dynamically. These calls, in turn, point to the Procedure Linkage Table (PLT), present in the .plt section of the binary. The .plt section contains actual x86 instructions that point directly to the GOT, which lives in the .got.plt section. GOT contains typically pointers that point to the actual location of these functions in the shared libraries in memory.

If a program contains a vulnerability allowing an attacker to write to this location in memory, this may be exploited to execute arbitrary code. Modern systems attempt to protect from this issue by implementing binary hardening security measures at build time, ensuring that the linker resolves all dynamically linked functions at the beginning of the execution and making the GOT read-only. This is called Relocation Read-Only (RELRO) and can be set as Partial RELRO (the default setting in gcc) that forces the GOT to come before the BSS in memory, eliminating the risk of a buffer overflow but still allowing for writes to happen, or Full RELRO, that makes the entire GOT read-only.

It’s possible to demonstrate this during runtime using gdb:

Figure 38- Writeable addresses from the GOT at runtime.

In this case, due to the Partial RELRO protection implemented, these are all writable locations. If the binary is compiled using this mode, this can be combined with MM functions to leak memory addresses of interest and then overwrite arbitrary GOT entries, which can be used to obtain arbitrary Remote Code Execution (RCE).

To exploit this vulnerability, a few addresses are required:

  • The GOT Entry Address for a function to replace
  • The resolved address for the function to be used in its place

Several MM functions can be used to either directly obtain or leak these addresses.

The MM_LOOKUP_SYM command can be used to obtain the resolved address of valid symbols:

Figure 39 – system libc address lookup.

While it doesn’t allow for a direct leak of GOT addresses, it provides enough information that they can be easily obtained from dumping and searching through memory using the MM_DUMP_IN_EVENT command.

First, the closest valid symbol name can be obtained from the binary to decrease the number of iterations required to find the correct addresses as much as possible. In this case, this will be CFE_PSP_BASE_MODULE_LIST:

Figure 40 – CFE_PSP_BASE_MODULE_LIST symbol location

Figure 41 – CFE_PSP_BASE_MODULE_LIST resolved address.

Using MM_LOOKUP_SYM, it is also possible to obtain the address for the first entry in the GOT, in this case, inet_ntop from libc (from Figure 38).

Figure 42 – inet_ntop resolved address.

Combining this information with MM_DUMP_IN_EVENT command allows an attacker to iterate over memory addresses, starting at CFE_PSP_BASE_MODULE_LIST (see Figure 43), dumping memory content until it matches the address pointing to inet_ntop. This will be the first entry for libc in memory.

Figure 43 – Dumping first 8 bytes from CFE_PSP_BASE_MODULE_LIST.

Once the offset between CFE_PSP_BASE_MODULE_LIST and a libc function is obtained, this can be used to overwrite arbitrary entries using the MM_FILL_MEM command:

Figure 44  – Overwriting an address with MM_FILL_MEM command.

The system libc function executes a command sent as an argument by calling /bin/sh in a child process (see Figure 45).

Figure 45 – system Linux documentation

At this point, since an attacker can replace any libc function with system, the only remaining requirement is to call the replaced function with a user-controlled input as an argument.

This can be achieved by replacing the memchr entry. Carefully reviewing the source code for the MM_LookupSymbolCmd showed that the symbol name string from the input is directly used in OS_strnlen, which uses it as an argument for memchr(see Figure 46). If the memchr entry is replaced with the address pointing at system, then this function will call /bin/sh with the argument the user inputted, leading to arbitrary Remote Command Execution.

Figure 46 – OS_strlen function.

This is demonstrated below by creating a file in the target host:

Figure 47 – Proof of Concept of the RCE.

Observing the iteration over memory addresses is possible until the correct address is found on the top panel. Then, the request for the addresses corresponding to the system and memchr, the GOT entry overwrite, and the command execution are followed. The bottom panel displays the execution of our Proof-of-Concept (PoC) for automatic exploitation (see Figure 47).

This issue can be summarised with CWE-123, which explains the write-what-where condition. This condition can be used to write data to areas of memory outside the scope of a policy, sometimes, like in this case, allowing us to execute arbitrary code [RD-11]. The disclosure of symbol addresses on the libc library is well detailed on CWE-497, where a product does not properly prevent sensitive system-level information from being accessed by unauthorized actors [RD-12].

Recommendations: To mitigate this vulnerability, it is essential to properly define a valid memory range for the application, as most memory manipulation functions rely on an initial check by CFE_PSP_MemValidateRange. This will limit the potential for leaking sensitive memory through MM functions, although other vulnerable methods may still exist. Forcing compilation with the FULL RELRO flag set provides complete protection by marking the GOT as read-only after all addresses are resolved at program startup, preventing modification of the GOT at runtime and significantly reducing the risk of RCE. If cFS is running on a platform or architecture that doesn’t provide this mitigation, the best approach is to implement an authentication mechanism to protect memory management functions. In such cases, the GOT might not be available, and other platform-specific attack vectors could be used to achieve similar results.

The CFE_ES_STOP_APP_CC and CFE_ES_START_APP_CC commands stop, unload, load, and start applications.

The CFE_ES_STOP_APP_CC command takes an application name as input, verifies if there is an application table record entry matching the given name, and if so, obtains its application ID before calling CFE_ES_DeleteApp with the converted ID.

As for the CFE_ES_START_APP_CC command, it takes the application name, application entry point, application file name, desired stack size, exception action, and priority level as inputs. It first validates if none of the names are empty strings, if the priority is lower than the maximum OS priority, and if the stack size was defined before calling CFE_ES_AppCreate with the specified parameters. CFE_ES_AppCreate then verifies if there is already an application table record entry matching the given name and, if not, reserves the first empty entry in the application table. It then sets the application running parameters based on the inputs and sets the Application record as in use together with updating CFE_ES_Global.LastAppId to the current resource being constructed. At this point, it loads the module into memory using the input parameters and starts the new application.

Issues appear when an application is not loaded properly due to bad parameters. If the application fails to start correctly, it does not perform adequate clean-up, leading to the application module being loaded and staying in memory while the application table entry is deleted.

The issue appears on lines 762-804 of cfe_es_apps.c:

Figure 48 – CFE_ES_AppCreate function for module loading and app startup.

The module is first loaded into memory using CFE_ES_LoadModule, which creates a module entry on the OS_module_table portion of the OS_common_table with the corresponding module name. The application is then started on a new task. If this is successful, the application record entry is tagged as in use. Otherwise, the entry is set as free and can be reused for other applications. At no point is the module unloaded if the application fails to start.

This makes it possible to stop an application and restart it so that the module is loaded and an active entry is created in the OS_module_table, but the application fails to start, leaving the entry in an active state with no way to remove it. Further attempts at starting an application with the same name will fail as the entry is already present and active.

This is demonstrated below using a running MM application as an example. Each step is accompanied by debugging screenshots that display the program state to contextualize the behavior described better.

At launch, and like with all other applications, the program contains an active MM application entry in the CFE_ES_Global.AppTable (Figure 49) and an active MM OS_common_table entry (Figure 50).

Figure 49 – Global App Table entry for the unmodified application.

Figure 50 – OS_module_table entry for the unmodified application.

The MM application is then stopped by sending a CFE_ES_STOP_APP_CC command (Figure 51). This, in turn, calls CFE_ES_DeleteApp,which is responsible for flagging the application for a safe stop and unloading unneeded resources. At this point, both the application entry in the  CFE_ES_Global.AppTable (Figure 52) and the OS_common_tableentry (Figure 53), have been deactivated, and their slots are free for reuse.

Figure 51 – Sending an application stop command.

Figure 52 – Global App Table entry for the stopped application.

Figure 53 – OS_module_table entry for the stopped application module.

At this stage, a new instance of the application can be requested to start using an invalid stack size by sending a CFE_ES_START_APP_CC command with a stack size of 0xFFFFFFFF(Figure 54).

While the ES module handles this case, and the error code corresponds to an invalid size (Figure 55), it’s important to note from the error message that the error is coming from CFE_ES_StartAppTask, which means that the application module was successfully loaded into memory (lines 762-770 on Figure 48).

Figure 54 – Restarting the application with an invalid stack size.

Figure 55 – osapi (-40) error code definition.

Because the program invalidates the application entry immediately after the application fails to start, this results in a situation where the module still exists in the OS_common_table(Figure 56), but the new CFE_ES_Global.AppTable entry that was being created is now deactivated, and the AppId is set to null (Figure 57).

Figure 56 – OS_module_table entry for the application with a bad stack size.

Figure 57 – Global App Table entry for the application with a bad stack size.

The result is that no other application can now be created using the same module name. Sending a CFE_ES_START_APP_CC command to start MM with a valid stack size (Figure 58) now results in an error (Figure 59).

Figure 58 – Attempt to restart the application with a valid stack size.

Figure 59 – osapi (-15) error code definition.

At the same time, because the application isn’t running and there is no entry in the application record table related to the module, it also can’t be removed using the CFE_ES_STOP_APP_CC command (Figure 60).

Figure 60 – Application fails to start with the correct stack size.

At this point, it is essential to note that this procedure creates new module entries in the module table section of OS_common_table that are not being properly removed. This table has limited slots available, as defined in the osapi-module.c (Figure 61). Each new application must add an entry to this section for proper execution. Filling all available ID slots makes it possible to block the creation of new applications.

Figure 61 – OS_module_table definition.

This can be achieved by repeating the process above multiple times to fill the OS_module_table with invalid entries. Since the default configuration has a maximum of 20odules, including the core system modules, the process can be repeated 20 times to fill the table (Figure 62).

Attempting to start a new valid application at this stage (Figure 63) results in a different error code (Figure 64).

Figure 62 – OS_module_table resource exhaustion.

Figure 63 – Attempt to load a new application after the DoS.

Figure 64 – osapi (-14) error code definition.

CWE-459 and CWE-771 describe the findings reported in this assessment. The first results from insufficient removal of temporary resources when a new application fails to start correctly (in our case, with an invalid stack size). [RD-13] In addition, this resource is kept allocated with no direct reference for removal, which allows for resource depletion by repeated calls [RD-14].

Recommendations: When an application loads successfully but fails to execute, it is recommended that proper clean-up steps be added before invalidating AppIDs. While CFE already includes code to remove unneeded resources upon application stop (CFE_ES_CleanUpApp), it only applies to applications listed in CFE_ES_Global.AppTable. Since the Application Record for a new application is immediately set as free after a failed start, it is crucial to ensure that all associated resources are properly handled beforehand.

The vulnerabilities discovered in cFS version Aquila highlight the critical need for robust cybersecurity measures in aerospace software systems. Remote Code Execution, Denial of Service, and Path Traversal vulnerabilities pose significant risks to mission integrity. Addressing these issues proactively will ensure spaceflight software’s continued security and reliability.

For further details and collaboration opportunities, contact the VisionSpace Cybersecurity Team.