Last update: 04-Nov-2020
Author: R. Koucha
Is "%m" format reentrant ?
Introduction

GCC and GLIBC provide various interesting extensions (i.e. specific features not compliant with the standards). Among them, there is the "%m" format specifier for the printf-like functions which displays the error message corresponding to the current value of the famous errno variable (Linux standard error code) as it is done with functions like strerror().

But strerror() is known to be not thread-safe. This paper aims at saying if "%m" is reentrant or not. In other words, if it is usable in multithreaded programs.

Usage

Most programs running in Linux environment display the current value of the errno variable in error messages when a system or library call fails. This is often useful to debug or at least understand why the program doesn't work as expected. For example:

#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
int fd;

  fd = open("/tmp/foo", O_RDONLY);
  if (fd < 0)
  {
    fprintf(stderr, "fopen(): Error %d\n", errno);
    return 1;
  }

  close(fd);

  return 0;
}

$ gcc example_1.c -o example_1
$ ./example_1 
fopen(): Error 2

Looking in the header files, we can see that this errno corresponds to the following error message:

$ cat /usr/include/asm-generic/errno-base.h
[...]
#define ENOENT  2; /* No such file or directory */
[...]

To be more explicit for the user, the previous program can be enhanced as follow to display the error message along with the error code thanks to the strerror() service:

#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>


int main(void)
{
int fd;

  fd = open("/tmp/foo", O_RDONLY);
  if (fd < 0)
  {
    fprintf(stderr, "fopen(): Error '%s' (%d)\n", strerror(errno), errno);
    return 1;
  }

  close(fd);

  return 0;
}

$ gcc example_2.c -o example_2
$ ./example_2
fopen(): Error 'No such file or directory' (2)

As error branches are numerous in programs, this is quite tedious to call strerror() service. Hence, GLIBC provides an extension to avoid calling it everywhere.

"%m": a shortcut for strerror()

The online manual of printf() says the following about "%m":

$ man 3 printf
[...]
 Conversion specifiers
[...]
       m  (Glibc extension; supported by uClibc and musl.)  Print output of strerror(errno).  No argument is required.

So, the previous program can be simplified as follow:

#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
int fd;

  fd = open("/tmp/foo", O_RDONLY);
  if (fd < 0)
  {
    fprintf(stderr, "fopen(): Error '%m' (%d)\n", errno);
    return 1;
  }

  close(fd);

  return 0;
}

$ gcc example_3.c -o example_3
$ ./example_3
fopen(): Error 'No such file or directory' (2)

By the way, we can notice that "%m" saves a parameter passing to the printing function. This contributes to reduce the generated code size when the source is composed with thousands (even more) error branches.

According to the Linux manual, "%m" hides a call to strerror(). But the latter is not thread-safe according to its manual:

$ man 3 strerror
[...]
DESCRIPTION
    The  strerror() function returns a pointer to a string that describes the error
    code passed in the argument errnum, possibly using the LC_MESSAGES part of the
    current locale to select in the argument errnum, possibly using the LC_MESSAGES
    part of the current locale to select the appropriate language. (For example,
    if errnum is EINVAL, the returned description will be "Invalid argument".) This
    string must not be modified by the application, but may be modified
    by a subsequent call to strerror() or strerror_l(). No other library
    function, including perror(3), will modify this string.

We could guess that strerror() must be reentrant as it should be implemented as a function returning the address of the entry  in a table of error messages indexed by errno. Something like this:

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

static char *err_msg[] =
{
  "error msg 0",
  "error msg 1",
  "error msg 2",
  "error msg 3",
  "NULL"
};


char *my_strerror(int errcode)
{
  return err_msg[errcode];
}


int main(int ac, char *av[])
{
int err;


  if (ac == 2)
  {
    err = atoi(av[1]);
    printf("Error '%s' (%d)\n", my_strerror(err), err);
    return 0;
  }

  return 1;
}

$  gcc example_4.c -o example_4
$ ./example_4 2
Error 'error msg 2' (2)

But the manual says that strerror() uses the LC_MESSAGES part of the current locale to select the appropriate language. So, it is not possible to store the error messages in the GLIBC as there are translated versions of them in multiple languages. That is why strerror() is based on a global static "malloced" buffer into which it copies the current error message thanks to strerror_r() (cf. glibc-2.29/string/strerror.c):

/* Return a string describing the errno code in ERRNUM.
   The storage is good only until the next call to strerror.
   Writing to the storage causes undefined behavior.  */
libc_freeres_ptr (static char *buf);   <------------------------ Global malloced static buffer

    
char *
strerror (int errnum)
{
  char *ret = __strerror_r (errnum, NULL, 0);
  int saved_errno;

    
  if (__glibc_likely (ret != NULL))
    return ret;
  saved_errno = errno;
  if (buf == NULL)
    buf = malloc (1024);
  __set_errno (saved_errno);

    
  if (buf == NULL)
    return _("Unknown error");

    
  return __strerror_r (errnum, buf, 1024);
}

Hopefully, a reentrant version of strerror() exists: strerror_r().

Does "%m" use strerror() or strerror_r() ?

printf()-like functions are defined in glibc-2.31/stdio-common in files with eponymous filenames. For example, printf() is defined in glibc-2.31/stdio-common/printf.c as:

/* Write formatted output to stdout from the format string FORMAT.  */
/* VARARGS1 */
int
__printf (const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = __vfprintf_internal (stdout, format, arg, 0);
  va_end (arg);

  return done;
}

#undef _IO_printf
ldbl_strong_alias (__printf, printf);
ldbl_strong_alias (__printf, _IO_printf);

They all call __vfprintf_internal() defined in glibc-2.31/stdio-common/vfprintf-internal.c:

[...]
/* Size of the work_buffer variable (in characters, not bytes.  */
enum { WORK_BUFFER_SIZE = 1000 / sizeof (CHAR_T) };
[...]
# define vfprintf	__vfprintf_internal
[...]
int
vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
  /* The character used as thousands separator.  */
  THOUSANDS_SEP_T thousands_sep = 0;

  /* The string describing the size of groups of digits.  */
  const char *grouping;

  /* Place to accumulate the result.  */
  int done;

  /* Current character in format string.  */
  const UCHAR_T *f;

  /* End of leading constant string.  */
  const UCHAR_T *lead_str_end;

  /* Points to next format specifier.  */
  const UCHAR_T *end_of_spec;

  /* Buffer intermediate results.  */
  CHAR_T work_buffer[WORK_BUFFER_SIZE];
  CHAR_T *workstart = NULL;
  CHAR_T *workend;
[...]
      /* Process current format.  */
      while (1)
	{
	  process_arg (((struct printf_spec *) NULL));
[...]
#define process_arg(fspec)						      \
      /* Start real work.  We know about all flags and modifiers and	      \
	 now process the wanted format specifier.  */			      \
[...]
      REF (form_strerror),	/* for 'm' */				      \
[...]
    LABEL (form_strerror):						      \
      /* Print description of error ERRNO.  */				      \
      string =								      \
	(CHAR_T *) __strerror_r (save_errno, (char *) work_buffer,	      \
				 WORK_BUFFER_SIZE * sizeof (CHAR_T));	      \
      is_long = 0;		/* This is no wide-char string.  */	      \
      goto LABEL (print_string)

The preceding source code shows that printf()-like functions actually use the reentrant version of strerror() that is to say strerror_r() which stores its string in the local work_buffer[].

Conclusion

The manual of the printf()-like functions is confusing as it says that "%m" prints the output of strerror(). This paper showed that "%m" is actually thread-safe as it uses strerror_r() (i.e. the reentrant version of strerror()).

About the author

The author is an engineer in computer sciences located in France. He can be contacted here.