Commit message: Security fix: avoid arbitrary code execution with 'm4 -F'.

* src/freeze.c (produce_frozen_state): Never pass raw file name as
printf format.
* NEWS: Document this fix.

Signed-off-by: Eric Blake <ebb9@byu.net>
(cherry picked from commit 031a71a80442ed2ad3c2ee14d5811c786a12c51b)
CVE: CVE-2008-1688
CWE: NVD-CWE-noinfo
Vulnerability description: Unspecified vulnerability in GNU m4 before 1.4.11 might allow context-dependent attackers to execute arbitrary code, related to improper handling of filenames specified with the -F option.  NOTE: it is not clear when this issue crosses privilege boundaries.

Project: m4

produce_frozen_state (const char *name)
{
  FILE *file;
  int h;
  symbol *sym;
  const builtin *bp;

  if (file = fopen (name, O_BINARY ? "wb" : "w"), !file)
    {
      M4ERROR ((warning_status, errno, "%s", name));
      return;
    }

  /* Write a recognizable header.  */

  xfprintf (file, "# This is a frozen state file generated by %s\n",
	   PACKAGE_STRING);
  xfprintf (file, "V1\n");

  /* Dump quote delimiters.  */

  if (strcmp (lquote.string, DEF_LQUOTE) || strcmp (rquote.string, DEF_RQUOTE))
    {
      xfprintf (file, "Q%d,%d\n", (int) lquote.length, (int) rquote.length);
      fputs (lquote.string, file);
      fputs (rquote.string, file);
      fputc ('\n', file);
    }

  /* Dump comment delimiters.  */

  if (strcmp (bcomm.string, DEF_BCOMM) || strcmp (ecomm.string, DEF_ECOMM))
    {
      xfprintf (file, "C%d,%d\n", (int) bcomm.length, (int) ecomm.length);
      fputs (bcomm.string, file);
      fputs (ecomm.string, file);
      fputc ('\n', file);
    }

  /* Dump all symbols.  */

  for (h = 0; h < hash_table_size; h++)
    {

      /* Process all entries in one bucket, from the last to the first.
	 This order ensures that, at reload time, pushdef's will be
	 executed with the oldest definitions first.  */

      symtab[h] = reverse_symbol_list (symtab[h]);
      for (sym = symtab[h]; sym; sym = SYMBOL_NEXT (sym))
	{
	  switch (SYMBOL_TYPE (sym))
	    {
	    case TOKEN_TEXT:
	      xfprintf (file, "T%d,%d\n",
			(int) strlen (SYMBOL_NAME (sym)),
			(int) strlen (SYMBOL_TEXT (sym)));
	      fputs (SYMBOL_NAME (sym), file);
	      fputs (SYMBOL_TEXT (sym), file);
	      fputc ('\n', file);
	      break;

	    case TOKEN_FUNC:
	      bp = find_builtin_by_addr (SYMBOL_FUNC (sym));
	      if (bp == NULL)
		{
		  M4ERROR ((warning_status, 0, "\
INTERNAL ERROR: builtin not found in builtin table!"));
		  abort ();
		}
	      xfprintf (file, "F%d,%d\n",
			(int) strlen (SYMBOL_NAME (sym)),
			(int) strlen (bp->name));
	      fputs (SYMBOL_NAME (sym), file);
	      fputs (bp->name, file);
	      fputc ('\n', file);
	      break;

	    case TOKEN_VOID:
	      /* Ignore placeholder tokens that exist due to traceon.  */
	      break;

	    default:
	      M4ERROR ((warning_status, 0, "\
INTERNAL ERROR: bad token data type in freeze_one_symbol ()"));
	      abort ();
	      break;
	    }
	}

      /* Reverse the bucket once more, putting it back as it was.  */

      symtab[h] = reverse_symbol_list (symtab[h]);
    }

  /* Let diversions be issued from output.c module, its cleaner to have this
     piece of code there.  */

  freeze_diversions (file);

  /* All done.  */

  fputs ("# End of frozen state file\n", file);
  if (close_stream (file) != 0)
    M4ERROR ((EXIT_FAILURE, errno, "unable to create frozen state"));
}

Project: m4

issue_expect_message (int expected)
{
  if (expected == '\n')
    M4ERROR ((EXIT_FAILURE, 0, "expecting line feed in frozen file"));
  else
    M4ERROR ((EXIT_FAILURE, 0, "expecting character `%c' in frozen file",
	      expected));
}

Project: m4

reverse_symbol_list (symbol *sym)
{
  symbol *result;
  symbol *next;

  result = NULL;
  while (sym)
    {
      next = SYMBOL_NEXT (sym);
      SYMBOL_NEXT (sym) = result;
      result = sym;
      sym = next;
    }
  return result;
}

Project: m4

reload_frozen_state (const char *name)
{
  FILE *file;
  int character;
  int operation;
  char *string[2];
  int allocated[2];
  int number[2];
  const builtin *bp;

#define GET_CHARACTER \
  (character = getc (file))

#define GET_NUMBER(Number) \
  do								\
    {								\
      (Number) = 0;						\
      while (isdigit (character))				\
	{							\
	  (Number) = 10 * (Number) + character - '0';		\
	  GET_CHARACTER;					\
	}							\
    }								\
  while (0)

#define VALIDATE(Expected) \
  do								\
    {								\
      if (character != (Expected))				\
	issue_expect_message ((Expected));			\
    }								\
  while (0)

  /* Skip comments (`#' at beginning of line) and blank lines, setting
     character to the next directive or to EOF.  */

#define GET_DIRECTIVE \
  do								\
    {								\
      GET_CHARACTER;						\
      if (character == '#')					\
	{							\
	  while (character != EOF && character != '\n')		\
	    GET_CHARACTER;					\
	  VALIDATE ('\n');					\
	}							\
    }								\
  while (character == '\n')

  file = m4_path_search (name, NULL);
  if (file == NULL)
    M4ERROR ((EXIT_FAILURE, errno, "cannot open %s", name));

  allocated[0] = 100;
  string[0] = xcharalloc ((size_t) allocated[0]);
  allocated[1] = 100;
  string[1] = xcharalloc ((size_t) allocated[1]);

  /* Validate format version.  Only `1' is acceptable for now.  */
  GET_DIRECTIVE;
  VALIDATE ('V');
  GET_CHARACTER;
  GET_NUMBER (number[0]);
  if (number[0] > 1)
    M4ERROR ((EXIT_MISMATCH, 0,
	      "frozen file version %d greater than max supported of 1",
	      number[0]));
  else if (number[0] < 1)
    M4ERROR ((EXIT_FAILURE, 0,
	      "ill-formed frozen file, version directive expected"));
  VALIDATE ('\n');

  GET_DIRECTIVE;
  while (character != EOF)
    {
      switch (character)
	{
	default:
	  M4ERROR ((EXIT_FAILURE, 0, "ill-formed frozen file"));

	case 'C':
	case 'D':
	case 'F':
	case 'T':
	case 'Q':
	  operation = character;
	  GET_CHARACTER;

	  /* Get string lengths.  Accept a negative diversion number.  */

	  if (operation == 'D' && character == '-')
	    {
	      GET_CHARACTER;
	      GET_NUMBER (number[0]);
	      number[0] = -number[0];
	    }
	  else
	    GET_NUMBER (number[0]);
	  VALIDATE (',');
	  GET_CHARACTER;
	  GET_NUMBER (number[1]);
	  VALIDATE ('\n');

	  if (operation != 'D')
	    {

	      /* Get first string contents.  */

	      if (number[0] + 1 > allocated[0])
		{
		  free (string[0]);
		  allocated[0] = number[0] + 1;
		  string[0] = xcharalloc ((size_t) allocated[0]);
		}

	      if (number[0] > 0)
		if (!fread (string[0], (size_t) number[0], 1, file))
		  M4ERROR ((EXIT_FAILURE, 0, "premature end of frozen file"));

	      string[0][number[0]] = '\0';
	    }

	  /* Get second string contents.  */

	  if (number[1] + 1 > allocated[1])
	    {
	      free (string[1]);
	      allocated[1] = number[1] + 1;
	      string[1] = xcharalloc ((size_t) allocated[1]);
	    }

	  if (number[1] > 0)
	    if (!fread (string[1], (size_t) number[1], 1, file))
	      M4ERROR ((EXIT_FAILURE, 0, "premature end of frozen file"));

	  string[1][number[1]] = '\0';
	  GET_CHARACTER;
	  VALIDATE ('\n');

	  /* Act according to operation letter.  */

	  switch (operation)
	    {
	    case 'C':

	      /* Change comment strings.  */

	      set_comment (string[0], string[1]);
	      break;

	    case 'D':

	      /* Select a diversion and add a string to it.  */

	      make_diversion (number[0]);
	      if (number[1] > 0)
		output_text (string[1], number[1]);
	      break;

	    case 'F':

	      /* Enter a macro having a builtin function as a definition.  */

	      bp = find_builtin_by_name (string[1]);
	      define_builtin (string[0], bp, SYMBOL_PUSHDEF);
	      break;

	    case 'T':

	      /* Enter a macro having an expansion text as a definition.  */

	      define_user_macro (string[0], string[1], SYMBOL_PUSHDEF);
	      break;

	    case 'Q':

	      /* Change quote strings.  */

	      set_quotes (string[0], string[1]);
	      break;

	    default:

	      /* Cannot happen.  */

	      break;
	    }
	  break;

	}
      GET_DIRECTIVE;
    }

  free (string[0]);
  free (string[1]);
  errno = 0;
  if (ferror (file) || fclose (file) != 0)
    M4ERROR ((EXIT_FAILURE, errno, "unable to read frozen state"));

#undef GET_CHARACTER
#undef GET_DIRECTIVE
#undef GET_NUMBER
#undef VALIDATE
}