Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 69

MISRA-C Pointer Safety Analysis

Recommended resources

Hello! Welcome to your tenth lesson.

In our previous session, we established the "what" and "why" behind key MISRA-C guidelines for pointer arithmetic and casting. You learned that these rules are not arbitrary but are foundational for building verifiably safe software, a principle that aligns directly with your work in ISO 26262 and ASPICE environments.

Today, we transition from theory to practice. Our learning outcome is to analyze C code to detect violations of critical MISRA-C pointer safety rules. You'll act as a senior code reviewer or a static analysis tool, using your knowledge to systematically hunt for non-compliances in code. We will expand on the rules from last time and introduce new ones related to pointer boundaries, object lifetimes, and more complex type conversions.

1. Pointer Boundaries and Relational Operations

One of the most frequent sources of critical bugs is out-of-bounds memory access. MISRA provides a set of strict rules to ensure pointer arithmetic and comparisons stay within well-defined memory regions.

Rule 18.1: Confining Pointer Arithmetic

While we briefly discussed Rule 18.4 (preferring [] over +), Rule 18.1 is the underlying safety principle: A pointer resulting from arithmetic on a pointer operand shall address an element of the same array as that pointer operand. This includes the address "one-past-the-end," but no further. Dereferencing that one-past-the-end pointer is strictly forbidden and leads to undefined behavior.

The following resource provides an excellent, in-depth analysis of this rule, including how static analysis tools interpret its nuances.

MISRA C:2023 Rule 18.1 - A pointer resulting from ...

This document from MathWorks provides a detailed breakdown of MISRA C:2023 Rule 18.1. It's especially useful for its examples that distinguish between calculating an address and accessing it.

Please read the 'Rule Definition', 'Rationale', and the 'Polyspace Implementation' sections. Pay close attention to the examples regarding structures and multidimensional arrays, as they highlight subtle but important distinctions.

From that reading, consider these key analysis points:

  • Calculating vs. Dereferencing: You can calculate a pointer to one-past-the-end of an array (e.g., int* p = &arr[10]; for a 10-element array), but you cannot dereference it (int x = *p; is a violation). This is crucial for loop termination conditions.
  • Structs and Arrays: The distinction between &myStruct.elem1[11] (compliant, as the address is within the larger myStruct object) and myStruct.elem1[11] (non-compliant, as the access is outside the elem1 array bounds) is a sophisticated point that trips up many developers.
  • Single Objects: A single variable is treated as an array of one element. Thus, int x; int* p = &x + 1; is compliant, but *p = 0; is not.

Rules 18.2 & 18.3: Pointer Subtraction and Comparison

Related to Rule 18.1 are the rules for how pointers can be compared.

  • Rule 18.2: Subtraction between pointers shall only be applied to pointers that address elements of the same array.
  • Rule 18.3: The relational operators > , >= , < and <= shall not be applied to objects of pointer type except where they point into the same object.
MISRA-C Rules for Pointers and Arrays
This image summarizes a few key MISRA rules. Note the bottom example for Rule 18.3, which illustrates the legal comparison of two pointers, `p` and `q`, pointing into the same array `a`.

The rationale is straightforward: comparing or subtracting addresses of unrelated objects (e.g., two different local variables, or variables in different memory segments) gives a result that is implementation-defined and has no portable meaning.

Test your understanding!

Analyze the following function. Identify at least three distinct violations of the MISRA rules we've just discussed (18.1, 18.2, 18.3). Explain your reasoning for each.

#include <stdint.h>
#include <stddef.h>

void analyze_buffers(void) {
    uint32_t primary_buf[8];
    uint32_t secondary_buf[8];

    uint32_t *p1 = &primary_buf[0];
    uint32_t *p2 = &primary_buf[8]; // One past the end
    uint32_t *p3 = &secondary_buf[4];

    // Check 1
    if (p1 < p3) { 
        /* do something */
    }

    // Check 2
    ptrdiff_t diff = p2 - p3;

    // Check 3
    *p2 = 0xDEADBEEF; 
}
Show answer
  1. Violation in Check 1: if (p1 < p3)

    • Rule: 18.3 (Relational operators shall only be applied to pointers pointing to the same object).
    • Reasoning: p1 points into primary_buf, while p3 points into secondary_buf. These are two different, unrelated array objects. Comparing their addresses is not portable and has no defined behavior in C.
  2. Violation in Check 2: ptrdiff_t diff = p2 - p3;

    • Rule: 18.2 (Subtraction between pointers shall only be applied to pointers that address elements of the same array).
    • Reasoning: Similar to the first violation, p2 and p3 point into different arrays. Subtracting them is not allowed.
  3. Violation in Check 3: *p2 = 0xDEADBEEF;

    • Rule: 18.1 (implied). This is a dereference of an out-of-bounds pointer.
    • Reasoning: p2 was initialized to &primary_buf[8], which is the address one-past-the-end of the array. While creating this pointer is compliant, dereferencing it to write a value is a classic buffer overflow and a serious violation.

2. Pointers and Object Lifetime

A particularly dangerous category of bugs involves pointers that outlive the data they point to. These are called dangling pointers.

Rule 18.6: Address of an Object with Automatic Storage

The rule states: The address of an object with automatic storage shall not be copied to another object that persists after the first object has ceased to exist.

"Automatic storage" refers to local variables stored on the stack. When a function returns, its stack frame is deallocated, and any pointers to variables within that frame become invalid.

The following resource provides concise examples of this common and critical error.

Safe and Secure Software - MISRA C:2012

The article 'Safe and Secure Software - MISRA C:2012' provides clear examples of Rule 18.6 violations.

Please scroll to the section 'Pointers and arrays' and focus on the code examples for Rule 18.6. Notice the two different ways a dangling pointer can be created: by returning it directly or by storing it in a more persistent location (like a global pointer).

Analyzing code for this violation means tracking where pointers are assigned. If you see the address-of operator (&) applied to a local variable, you must check if that address is:

  1. Returned from the function.
  2. Assigned to a global or static pointer.
  3. Assigned to a pointer passed in as a function argument which is then stored elsewhere.

This type of bug can be maddening to debug, as the memory location might work correctly for a short time until the stack space is overwritten by a subsequent function call—a classic symptom you might have encountered when debugging with Trace32.


3. A Deeper Look at Pointer Conversion Violations

In the last lesson, we covered the most common casting rules. Now, let's broaden our analytical scope to spot a wider range of conversion-related violations. The following reading covers a catalog of these rules.

Safe and Secure Software - MISRA C:2012

Let's revisit 'Safe and Secure Software - MISRA C:2012' to review a comprehensive list of pointer conversion rules.

Read the entire 'Pointer type conversions' section (Rules 11.1 through 11.9). We covered some of these last time, but pay special attention to the rules you haven't seen: 11.1 (function pointers), 11.5 (void*), and 11.8 (const/volatile).

Let's highlight a few key rules for analysis:

  • Rule 11.1 (Function Pointers): Casting between different function pointer types or between a function pointer and an object/integer pointer is forbidden. This is critical for event-driven systems or callback architectures (common in AUTOSAR) where type-safe function dispatch is essential.
  • Rule 11.5 (void to object):** A conversion from void* to a pointer to object should not be implicit. This is a Required rule, meaning it can be deviated with justification. In C, malloc returns void*, and assigning its result to int *p is an implicit conversion. MISRA C:2012 requires an explicit cast (int *p = (int*)malloc(...)), though this is a point of debate in the wider C community. The key is that any such conversion must be deliberate and auditable.
  • Rule 11.8 (Casting away qualifiers): A cast must not remove const or volatile. This is a mandatory safety rule. Casting away const can lead to modification of what should be read-only data. Casting away volatile is even more dangerous in an embedded context, as it tells the compiler it can optimize away reads/writes to a hardware register, breaking device driver logic.
Test your understanding!

You are reviewing a colleague's code for a generic message dispatcher. Identify the MISRA violations related to the conversion rules we just discussed.

#include <stdint.h>
#include <stdlib.h>

// Type for a generic handler
typedef void (*generic_handler_t)(void *data);

// Type for a specific handler
typedef void (*int_handler_t)(int *data);

void process_int(int *data) { /* ... */ }

void register_handler(generic_handler_t *handler_slot, const void *new_handler) {
    // Violation 1?
    *handler_slot = (generic_handler_t)new_handler; 
}

void dispatch(void) {
    const int payload = 123;
    int_handler_t specific_handler;

    // Violation 2?
    register_handler((generic_handler_t*)&specific_handler, &process_int);
    
    // Violation 3?
    specific_handler = (int_handler_t) &payload; 
}
Show answer
  1. Violation 1: *handler_slot = (generic_handler_t)new_handler;

    • Rule: 11.8 (A cast shall not remove const qualification).
    • Reasoning: The new_handler parameter is a const void*, but it's being cast to generic_handler_t, which is a void (*)(void*). This cast removes the const qualifier from the object it points to (the function itself). While function pointers and const can be complex, this pattern is flagged by MISRA as it discards a qualifier. A better function signature would be const generic_handler_t new_handler.
  2. Violation 2: register_handler((generic_handler_t*)&specific_handler, &process_int);

    • Rule: 11.3 (A cast shall not be performed between a pointer to object type and a pointer to a different object type). This is a subtle instance.
    • Reasoning: &specific_handler is a pointer to a function pointer (int_handler_t*). It is being cast to a different pointer-to-function-pointer type (generic_handler_t*). This is a cast between two different pointer types and is a violation.
  3. Violation 3: specific_handler = (int_handler_t) &payload;

    • Rule: 11.1 (Conversions shall not be performed between a pointer to function and any other type).
    • Reasoning: This code is casting a pointer-to-object (const int*, the address of payload) into a pointer-to-function (int_handler_t). This is a severe type confusion. Attempting to call specific_handler would try to execute data as code, leading to a crash.

Conclusion

Today you put on the hat of a static analysis tool, learning to spot critical MISRA C violations in code. We've seen that compliant pointer usage is about discipline: respecting array boundaries, understanding object lifetimes, and preserving the integrity of the type system.

Key Takeaways for Code Analysis:

  • Boundary Checks: Always verify that pointer arithmetic, subtraction, and comparisons are confined to a single, valid object. Be extra vigilant about off-by-one errors that lead to dereferencing a one-past-the-end pointer.
  • Scope and Lifetime: When you see the address of a local (stack) variable being taken, trace its usage. If it's returned or stored in a location with a longer lifetime, it's a dangling pointer violation (Rule 18.6).
  • Conversion Scrutiny: Treat every pointer cast with suspicion. Check for conversions between incompatible function pointers (Rule 11.1), casts that discard qualifiers like const or volatile (Rule 11.8), and improper mixing of pointers and integers (Rule 11.4).

Mastering this analytical skill is invaluable. It not only helps you write better code but also allows you to more effectively review team members' code and correctly interpret the output from tools like Coverity and Helix QAC.


Preview of the Next Lesson

Detecting violations is the first half of the battle. The second, more constructive half, is fixing them. In our next lesson, we will focus on the final learning outcome of this module: Refactor non-compliant code involving pointers to meet MISRA-C standards and functional safety goals. We will take the non-compliant snippets from today and work through safe, compliant, and efficient alternatives.