0

MISRA C 2012 Directive 4.1 says that Run-time failures should be minimized and further states for pointer dereferencing that a pointer should be checked for NULL before it's dereferenced, unless it's already known to be not NULL.

When writing a library performing simple operations, checking the input pointers in each library function for NULL creates larger and therefore less understandable code even for simple operations.

For example below library computes the squared euclidian norm of a vector and gets used in a controller

/*** Option 1: Checking pointers ***/

Library:

/* file vector.c */

bool_t bVectorNormSq(float32_t pf32Vec[], uint8_t u8Len, float32_t *pf32NormSq)
{
    uint8_t u8n;
    bool bRet = false;

    /* Check pointers */
    if( (pf32Vec != NULL) && (pf32NormSq != NULL) )
    {
        *pf32NormSq = 0.0f;

        for(u8n = 0U; u8n < u8Len; u8n++)
        {
            *pf32NormSq += (pf32Vec[u8n] * pf32Vec[u8n]);
        }
        bRet = true;
    }
    else
    {
        /* Do not alter pf32NormSq (unknown if valid pointer) */
        bRet = false;
    }

    return bRet;
}
/* EOF */

Consumer of library:

/* file controller.c */

/* ... */

bool_t bControllerStep(void)
{
    float32_t pf32MyVec[3] = { 0 };
    float32_t f32MyNorm = 0.0f;

    /* ... */

         /* MISRA C 2012 Rule 17.7, Call will always be successful, thus return value not checked */
     (void)bVectorNormSq(pf32MyVec, 3U, &f32MyNorm); 
    
         /* ... */

}

/* EOF */

/*** Option 2: Not checking pointer, responsibility to supply valid inputs placed on caller ***/

Library:

/* file vector.c */

/**
 * @note This library assumes that valid pointers will be supplied,
 *       pointers are NOT checked before they are used.
 */

/* Assert macro expands to "(void)(CONDITION)" for NDEBUG defined */
#ifdef NDEBUG
  VECTOR_ASSERT( CONDITION ) (void)(CONDITION)
#else
  /* ... */
#endif /* NDEBUG */

float32_t f32VectorNormSq(float32_t pf32Vec[], uint8_t u8Len)
{
    float32_t f32Norm = 0.0f;
    uint8_t u8n = 0U;

    VECTOR_ASSERT(pf32Vec!=NULL);

    for(u8n = 0U; u8n < u8Len; u8n++)
    {
        f32NormSq += (pf32Vec[u8n] * pf32Vec[u8n]);
    }

}

/* EOF */

Consumer of library:

/* file controller.c */

/* ... */

bool_t bControllerStep(void)
{
    float32_t pf32MyVec[3] = { 0 };
    float32_t f32MyNorm = 0.0f;

    /* ... */

    f32MyNorm = f32VectorNormSq(pf32MyVec, 3U);

    /* ... */

}

/* EOF */

For option 1 the library function f32VectorNormSq() can be reasoned by the caller bControllerStep() to always execute successfully (return true), because the array pf32MyVec is defined in the caller and pf32MyVec thus can't be NULL. Therefore the caller chooses to ignore the return value.

Above reasoning is likely to be the applicable for many uses of the library, resulting in callers frequently ignoring return values for option 1. This could lead to a programmer being complacend in ignoring return values, even if they shouldn't, e.g. because a zero division was detected by the library.

Opposed to this option 2 assumes the caller supplying valid pointers, documents this in the library /* @note...*/ and only uses a assert for NULL pointers to assist during development, later to be disabled for deployment. In this option a Boolean return value being present highlights to the programmer that the library operation might fail for reasons other than invalid pointers or incorrect other trivial inputs, e.g due to zero division and care should be taken before using the computed values, avoiding the risk of complacency.

Therefore my question(s):

  • Is it situations as in the example shown compliant to MISRA C for a libary to neglect checking pointers or other trivial inputs?

  • Are there any additional conditions that have to be fullfilled, besides documenting the missing input checking in the source code, e.g. is a formal deviation needed?

Application background:

This is just an aerospace student with focus in control systems, trying to understand how things are done properly in the actual coding of control systems, by writing his own control algorithms as if they were running on the real thing i.e. by following standards like MISRA C.

Code written by me will NOT be going on a live system anytime soon, but for the purpose of your answers please consider this code as running on the actual system where a failure of the function is classified as catastrophic, .i.e. everybody dies.

I'm aware of the whole software and hardware engineering (SAE ARP4754, SAE ARP4761, DO-178C, ...) around the actual implementation process. This question is really just about the instructions executing on the hardware, not about the need for redudant & dissimilar hardware or requirements, reviews, testing, ...

EDIT: The library in question is low in the code stack thus sanitizing code for inputs from the outside (sensors,...) can be expected to be present. I'm trying to avoid falling prey to Cargo cult programming by blindly following the rule "Always check all inputs".

VictorS
  • 3
  • 3
  • 2
    If you want to know how it is done **properly** why do you ask if it is somehow acceptable to remove all that stuff that makes the program more robust and aims to avoid hard to find errors? If you are interested in code "running on the real thing" (in the area of aerospace) then you should not even think a second about "removing checks", "relying on someone else" etc. – Gerhardh Nov 17 '22 at 13:59
  • The proper way to deal with all such scenarios is simply to make sure there is no pointer pointing at unknown data at any point. In case setting a pointer to null is a documented way of keeping it at a non-initialized or failsafe state, then check for null where appropriate. If not, then there's no need to add random null pointer checks everywhere - they just add bloat, needless complexity and slower execution. – Lundin Nov 17 '22 at 14:03
  • @ Gerhardh My question is motivated by a gut feeling going in the direction of Lundin's comment as needless complexity (NULL checks everywhere) also adds potential for new bugs. Needless because the return value in option 1 isn't actually used, because the caller knows the pointer to be valid beforehand. I have updated the question to hopefully better reflect this. – VictorS Nov 17 '22 at 15:11
  • @Lundin If you format your comment as answer and could add some remarks about the need for documentation, this would make a valid answer for me. – VictorS Nov 17 '22 at 15:16
  • 1
    Note that error handling is a vast topic, with many [possible](https://godbolt.org/z/Yq3dYT3Yr) approaches (and even more opinions). – Bob__ Nov 17 '22 at 15:59
  • If you have no control over the caller of your library's users, insert the checks. If you have control, for example because you designed the library as simplification of the build, and you are sure that such a pointer will never be `NULL`, omit the check. Decide as if your own life depends on it. – the busybee Nov 17 '22 at 19:15
  • Some compilers insert such checks automatically, if given certain options. Last year I had to use such a compiler and the rules were to use that option. – the busybee Nov 17 '22 at 19:15
  • 2
    If such checks lower the performance below the requested (= needed) performance, you are already calling for disaster. Never trade safety against cost or convenience, especially if you deal with human health. – the busybee Nov 17 '22 at 19:15
  • @thebusybee You always have control over the caller, it's called source code documentation. If you document that your parameter x expects a pointer to an allocated variable and someone passes a bad pointer instead, the problem is in the calling code. And that's where the error checks should be too - as close as possible to the place where some pointer might get the wrong value for whatever reason - not inside some completely unrelated library. – Lundin Nov 17 '22 at 21:06
  • Demanding that some unrelated lib should check if your pointers are null is as silly as first putting up a big, clearly visible sign "no dogs allowed" outside the supermarket, then despite that stop every customer on their way in to check if they have a dog hidden inside a bag or underneath their coat. Blocking all traffic flow for something that won't even happen. Just trust the customers to not bring one. If they fear that they will accidentally bring a dog along despite it all, well then _they_ could add a routine to check that they aren't bringing one before entering the store... – Lundin Nov 17 '22 at 21:11
  • 1
    @Lundin Well, that might be your utopia, in real life we are bound to regulations and standards. These were invented mostly for good reasons, only few because they did not know better. I'd rather have multiple redundant checks executed than losing my health or even life. – the busybee Nov 17 '22 at 21:15
  • 1
    @thebusybee Again, my whole argument is that such checks should be as close to the place where you suspect that a pointer might get assigned to null for whatever reason. Generally, it's so much more likely that a pointer gets assigned an invalid, non-zero value, because of bugs or other sources of memory corruption. – Lundin Nov 17 '22 at 21:25
  • And one counter-measure against that is to that is to use bug-killing coding standards like MISRA C. Another is to use MMU virtual memory or software traps that handle strange accesses gracefully. And so on, there are countless of effective ways, I once wrote a checklist here: https://stackoverflow.com/a/36892379/584518. Notably, spewing null pointer checks inside unrelated libraries is absent from the list. – Lundin Nov 17 '22 at 21:25
  • Also to find non-initialized/null-initialized variables used by accident, use static analysis. The C language even has a little bit of support for it, even though most compilers still do not: `void some_func (size_t n, const char str [static n])` ... `some_func(x, 0); //clang warning: null passed to a callee that requires a non-null argument`. But this is only a compile-time check. – Lundin Nov 17 '22 at 21:37
  • @Lundin I'm indeed using a static analysis tool (cppcheck). Not sure if it's any good, but it's the one I have access to and still better then nothing. I like the use of additional compile time checks for NULL pointers. Unfortunatly use of static is banned inside [] for array parameters by MISRA Rule 17.6, which is mandatory. – VictorS Nov 18 '22 at 17:54

1 Answers1

0

Regarding Option 1 - how would you write the documentation to that function? Does this make sense:

bVectorNormSq
Description of what this function does here.

pf32Vec    A pointer to an allocated array of [u8Len] that will-...
u8Len      The size of the array [pf32Vec] in bytes.
...

Returns:   true if successful, false in case pf32Vec was a null pointer.

It doesn't make sense. You already documented that pf32Vec must be a pointer to an allocated array so why would the usernot read that part, but read the part about return status? How are we even supposed to use this function?

bool result = bVectorNormSq(some_fishy_pointer, ...); // checks for null internally

if(result == false)
{
  /* error: you passed a null pointer */
}

Why can't you instead write that very same code like this?

if(some_fishy_pointer == NULL)
{
 /* handle the error instead of calling the function in the first place */
}
else
{
  bVectorNormSq(some_fishy_pointer, ...); // does not check for null internally
}

It's the same amount of error checking, same amount of branches. Literally the only difference is the slower execution speed of the first version.


Also, your extra error checks add extra complexity which could in turn lead to extra bugs.

Potential bugs:

if( (pf32Vec = NULL) ||(pf32NormSq = NULL) )

or

if( (pf32Vec =! NULL) && (pf32NormSq != NULL) )

or (not just a potential bug):

bool_t ... bool bRet = false; ... return bRet;


Extra error checking is a good thing, but place it where it matters - sanitize data at the point where it gets assigned. Example:

const char* ptr = strstr(x,y);
if(ptr == NULL) // Correct! Check close to the location where the pointer is set
{}
...
some_function(ptr);
...
void some_function (const char* str)
{
  if(str == NULL) // Not correct. This function has nothing to do with the strstr call.

}
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • The strange/confusing looking caller code you showed above indeed promted my question in the first place, thinking "This can't be right ...". – VictorS Nov 18 '22 at 17:58
  • I have now adopted option 2 for the reason given in the comments under the question justified by: 1. I have complete control over the library and any potential callers as all written by me, 2. Only static memory allocation is used, no NULL pointers anywhere for error reporting. – VictorS Nov 18 '22 at 18:01
  • There is a school of thought called *design by contract* - the library function states its assumptions, and the caller is responsible for complying with the contract. Unfortunately, programmers are lazy (see also return value checking from `malloc` and `printf`) – Andrew Nov 20 '22 at 00:43