Environmets:

  • compiler: x86_64-linux-gnu 7.4.0, x86_64-linux-gnu 4.8.5
  • assembly: AT&T
  • code base: glibc 2.23.90

All content is written based on GNU C.

As most people know, NULL is not zero in GNU C.

NULL != 0

So what is NULL? Can easily check it by using sample code.

Check with sample code

-> sample source code: null.c

#include <stddef.h>

int main(void) {
	char *value = NULL;

	return 0;
}

-> preprocess only

$ gcc -E null.c

-> result

# 1 "null.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "null.c"
# 1 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 1 3 4
.....

# 3 "null.c"
int main(void) {
 char *value = 
# 4 "null.c" 3 4
	((void *)0) /* 0 == ((void *)0) */
...
}

As we can see from the results of pre-process, NULL was replace by ((void *)0).

NULL == ((void *)0)

What happens if include stdio.h instead of stddef.h?

-> include stdio.h instead of stddef.h

#include <stdio.h>

int main(void) {
	char *value = NULL;

	return 0;
}

-> preprocess only

$ gcc -E null.c

-> result

# 4 "null.c"
int main(void) {
 char *value = 
# 5 "null.c" 3 4
	((void *)0)
...
}

The results are the same. This is because NULL is not defined in stdio.h, and just include stddef.h. If NULL is defined in stdio.h, it is redefined by stddef.h.

-> stddef.h is also included by stdio.h

# 1 "null.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "null.c"

# 1 "/usr/include/stdio.h" 1 3 4
...
# 1 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 1 3 4
...

-> content of stdio.h

#define __need_size_t
#define __need_NULL
#include <stddef.h>
...

Looking at stddef.h in gcc, NULL is defined as follows.

-> NULL definition in stddef.h

/* A null pointer constant.  */

#if defined (_STDDEF_H) || defined (__need_NULL)
#undef NULL		/* in case <stdio.h> has defined it. */
#ifdef __GNUG__
#define NULL __null
#else   /* G++ */
#ifndef __cplusplus
#define NULL ((void *)0)
#else   /* C++ */
#define NULL 0
#endif  /* C++ */
#endif  /* G++ */
#endif	/* NULL not defined and <stddef.h> or need NULL.  */
#undef	__need_NULL

Note that in GNU C++ compiler1 NULL is defined as __null, and in C++2 defined as 0.

-> check with GNU C++

$ g++ -E null.c

-> result with GNU C++<

# 4 "null.c"
int main(void) {
 char *value = 
# 5 "null.c" 3 4
	__null
...

Then,
Does the problem occur when use 0 instead of NULL or vice versa?
The most significant difference between 0 and NULL is that 0 is int and NULL is void *
. In other words, on a 64-bit machine, the sizes of 0 and NULL will be different.

-> sizeof NULL in 64-bit

#include <stdio.h>

int main(void) {
	printf("sizeof(0): %lu, sizeof(NULL): %lu\n", sizeof(0), sizeof(NULL));

	return 0;
}

-> size of result

sizeof(0): 4, sizeof(NULL): 8

Therefore, putting a 0 in the expectation of NULL makes it possible to pop an un-intended value of 4-bytes. The most easily conceivable situation is va_arg (val, *).

-> problem examples when put 0 not NULL

#include <stdio.h>
#include <stdarg.h>

int expect(const char *fmt, ...) {
	va_list ap;
	char *val;

	va_start(ap, fmt);

	while (val = va_arg(ap, char *))
		printf("value: %s\n", val);

	va_end(ap);

	return 0;
}

int main(void) {
  /* because of x86-64bit calling convention,
        passed many arguments for reproduction. */
	expect("aaa", "bbb", "ccc", "ddd", "fff", "ggg", "hhh", "iii", 0);

	return 0;
}

va_arg expects a char* pointer with a va argument, so it will try to pop 8-bytes on the stack. But if put 0 in place of NULL, 4-byte stack overflow will occur.

-> compile with gcc-4

$ gcc-4.8 -o null null.c

-> result

value: bbb
value: ccc
value: ddd
value: fff
value: ggg
value: hhh
value: iii
Segmentation fault (core dumped)

-> change to

//expect("aaa", "bbb", "ccc", "ddd", "fff", "ggg", "hhh", "iii", 0);
expect("aaa", "bbb", "ccc", "ddd", "fff", "ggg", "hhh", "iii", NULL);

-> result

value: bbb
value: ccc
value: ddd
value: fff
value: ggg
value: hhh
value: iii

For reference, In gcc-7, That issues can not be reproduced. looked at the code compiled with gcc-7, compiled to use pushq when passing arguments instead of push. The compiler seems to be smarter.

...
7d9:	6a 00                	pushq  $0x0
7db:	48 8d 05 f5 00 00 00 	lea    0xf5(%rip),%rax        # 8d7 
...

The functions of the execl family are guided by the that should pass NULL instead of 0 like this.


man execl
The const char *arg and subsequent ellipses in the execl(), execlp(), and execle() functions can be thought of as arg0, arg1, ..., argn.

Together they describe a list of one or more pointers to null-terminated strings that represent the argument list available to the executed program.

The first argument, by convention, should point to the filename associated with the file being executed.
The list of arguments must be terminated by a `null pointer`, and, since these are variadic functions, this pointer must be `cast (char *) NULL`.

The execl implementation of glibc shows why. The execl function expects NULL to counter argc.

-> glibc/posix/execl.c

/* Execute PATH with all arguments after PATH until
   a NULL pointer and environment from `environ'.  */
int
execl (const char *path, const char *arg, ...)
{
  ptrdiff_t argc;
  va_list ap;
  va_start (ap, arg);
  for (argc = 1; va_arg (ap, const char *); argc++)
    {
      if (argc == INT_MAX)
	{
	  va_end (ap);
	  errno = E2BIG;
	  return -1;
	}
    }
  va_end (ap);
....
jooojub.

  1. GNU C++ = (GNUG __= GNUC__&&__cplusplus) 

  2. C++ = (__cplusplus) 

Requires :

  • compiler: gcc 3.3.1 later

In systemd code, we can find may attribute keywords. Among them, I decided to take a closer look at cleanup, which may be useful for security coding.

It says so on the gnu gcc documents.


cleanup
The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored.

If -fexceptions is enabled, then cleanup_function is run during the stack unwinding that happens during the processing of the exception. Note that the cleanup attribute does not allow the exception to be caught, only to perform an action. It is undefined what happens if cleanup_function does not return normally.

ref. https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html

The following phrases are noticeable:

The cleanup attribute runs a function when the variable goes out of scope

It means, if used well, we will be able to prevent situations where a leak occur due to failure to maintain the pair like {malloc/free, open/close, …}

Check with code

-> sample source code: simple

#include <stdio.h>

void auto_function(int *arg) {
	printf("%s: called by __clean_up__: %d\n", __func__, *arg);

	return;
}

int main(int argc, char **argv) {
	__attribute__ ((__cleanup__(auto_function))) int val = 5;

	return 0;
}

-> result

auto_function: called by __clean_up__: 5

-> assembly: x86_64 AT&T

00000000004005c1 <main>:
  ...
  4005ec:	48 8d 45 e4             lea    -0x1c(%rbp),%rax
  4005f0:	48 89 c7                mov    %rax,%rdi
  4005f3:	e8 9e ff ff ff          callq  400596 <auto_function>
  ...

We can see that auto_function(& val) was called automatically.
In other words, we can called the free() or close() without forgetting.

-> sample source code: fclose

...
void fclosep(FILE **f) {
	fclose(f);
}

int main(int argc, char **argv) {
	__attribute__ ((__cleanup__(fclosep))) FILE *f = fopen(name, "r");
    ...
    /* We don't need to call fclose(f) manually */

	return 0;
}

The point at which the function is called by the __cleanup__ attribute is important.
The document specifies The cleanup attribute runs a function when the variable goes out of scope. Let’s check.

-> sample source code: scope

#include <stdio.h>
#include <stdlib.h>

void freep(void *p) {
	free(*(void **) p);
	printf("value freed\n");
}

int main(int argc, char **argv) {
	{
		__attribute__ ((__cleanup__(freep))) void *p = malloc(10);
	}

	printf("before return\n");

	return 0;
}

-> result

value freed
before return

-> assembly: x86_64 AT&T

  ...
  40066a:	31 c0                	xor    %eax,%eax
	{
		__attribute__ ((__cleanup__(freep))) void *p = malloc(10);
  40066c:	bf 0a 00 00 00       	mov    $0xa,%edi
  400671:	e8 9a fe ff ff       	callq  400510 <malloc@plt>
  400676:	48 89 45 f0          	mov    %rax,-0x10(%rbp)
  40067a:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  40067e:	48 89 c7             	mov    %rax,%rdi
     # The function is called by the life cycle of the variable
  400681:	e8 a0 ff ff ff       	callq  400626 <freep>
	}

	printf("before return\n");
  400686:	bf 40 07 40 00       	mov    $0x400740,%edi
  40068b:	e8 50 fe ff ff       	callq  4004e0 <puts@plt>

	return 0;
  400690:	b8 00 00 00 00       	mov    $0x0,%eax
}

So, be careful to make the following mistakes.

-> sample source code: be careful with scope

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void freep(void *p) {
	free(*(void **) p);
}

void *new_buffer(int size) {
	__attribute__ ((__cleanup__(freep))) int *p = malloc(size);

	return p;
}

int main(int argc, char **argv) {
	char *value = NULL;

	value = (char *)new_buffer(5);
	strncpy(value, "test", 5);

	printf("value: %s\n", value);

	return 0;
}
Segmentation fault (core dumped)
jooojub.