>From 0e6af5dac2f6413011dea2b26fc2dfc162306cef Mon Sep 17 00:00:00 2001
From: Stef Walter <stefw@redhat.com>
Date: Tue, 7 Jan 2014 14:44:11 +0100
Subject: [PATCH 1/2] util: A safe printf for user provided format strings

Since the default printf(3) implementation cannot safely be
used on user (or admin) provided input, this is a safe implementation.

This will be used in later patches by the full_name_format option
The implementation came from realmd, but only has libc dependencies.

The number of fields is pre-defined, and safe printf fails if
an invalid field is accessed.

Only string fields are supported, and only flags relevant to string
fields are supported. Width and precision work as expected, but
precision cannot read from a field.

Tests are included, and ported to the check based testing that
sssd uses.
---
 .gitignore                    |   1 +
 Makefile.am                   |  16 +-
 src/tests/safe-printf-tests.c | 273 ++++++++++++++++++++++++++++++++
 src/util/safe-printf.c        | 352 ++++++++++++++++++++++++++++++++++++++++++
 src/util/safe-printf.h        |  80 ++++++++++
 5 files changed, 721 insertions(+), 1 deletion(-)
 create mode 100644 src/tests/safe-printf-tests.c
 create mode 100644 src/util/safe-printf.c
 create mode 100644 src/util/safe-printf.h

diff --git a/.gitignore b/.gitignore
index a9515ac..74d5dee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,6 +95,7 @@ ad_common_tests
 ad_ldap_opt-tests
 dlopen-tests
 dyndns-tests
+safe-printf-tests
 fqnames-tests
 krb5-child-test
 nss-srv-tests
diff --git a/Makefile.am b/Makefile.am
index 4015dad..f6635d6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -136,7 +136,8 @@ if HAVE_CHECK
         debug-tests \
         ipa_hbac-tests \
         sss_idmap-tests \
-        responder_socket_access-tests
+        responder_socket_access-tests \
+        safe-printf-tests
 
 if BUILD_SSH
     non_interactive_check_based_tests += sysdb_ssh-tests
@@ -400,6 +401,7 @@ dist_noinst_HEADERS = \
     src/util/util.h \
     src/util/io.h \
     src/util/util_errors.h \
+    src/util/safe-printf.h \
     src/util/strtonum.h \
     src/util/sss_endian.h \
     src/util/sss_nss.h \
@@ -572,6 +574,7 @@ libsss_util_la_SOURCES = \
     src/sbus/sssd_dbus_server.c \
     src/util/util.c \
     src/util/memory.c \
+    src/util/safe-printf.c \
     src/util/server.c \
     src/util/signal.c \
     src/util/usertools.c \
@@ -1179,6 +1182,17 @@ util_tests_LDADD = \
     $(SSSD_INTERNAL_LTLIBS) \
     libsss_test_common.la
 
+safe_printf_tests_SOURCES = \
+    src/tests/safe-printf-tests.c
+safe_printf_tests_CFLAGS = \
+    $(AM_CFLAGS) \
+    $(CHECK_CFLAGS)
+safe_printf_tests_LDADD = \
+    $(SSSD_LIBS) \
+    $(CHECK_LIBS) \
+    $(SSSD_INTERNAL_LTLIBS) \
+    libsss_test_common.la
+
 debug_tests_SOURCES = \
     src/tests/debug-tests.c \
     src/tests/common.c
diff --git a/src/tests/safe-printf-tests.c b/src/tests/safe-printf-tests.c
new file mode 100644
index 0000000..b5b9c62
--- /dev/null
+++ b/src/tests/safe-printf-tests.c
@@ -0,0 +1,273 @@
+/*
+ * This file originated in realmd
+ *
+ * Copyright 2012 Red Hat Inc
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * See the included COPYING file for more information.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+
+#include "src/util/safe-printf.h"
+
+#include <check.h>
+#include <popt.h>
+#include <string.h>
+#include <talloc.h>
+
+typedef struct {
+    const char *format;
+    const char *args[8];
+    const char *result;
+} Fixture;
+
+static const Fixture fixtures[] = {
+    {
+      /* Just a bog standard string */
+      "%s", { "blah", NULL, },
+      "blah"
+    },
+    {
+      /* Empty to print */
+      "%s", { "", NULL, },
+      ""
+    },
+    {
+      /* Nothing to print */
+      "", { "blah", NULL, },
+      ""
+    },
+    {
+      /* Width right aligned */
+      "%8s", { "blah", NULL, },
+      "    blah"
+    },
+    {
+      /* Width left aligned */
+      "whoop %-8s doo", { "dee", NULL, },
+      "whoop dee      doo"
+    },
+    {
+      /* Width space aligned (ignored) */
+      "whoop % 8s doo", { "dee", NULL, },
+      "whoop      dee doo"
+    },
+    {
+      /* Width left space aligned (ignored) */
+      "whoop % -8s doo", { "dee", NULL, },
+      "whoop dee      doo"
+    },
+    {
+      /* Precision 1 digit */
+      "whoop %.3s doo", { "deedle-dee", NULL, },
+      "whoop dee doo"
+    },
+    {
+      /* Precision, N digits */
+      "whoop %.10s doo", { "deedle-dee-deedle-do-deedle-dum", NULL, },
+      "whoop deedle-dee doo"
+    },
+    {
+      /* Precision, zero digits */
+      "whoop %.s doo", { "deedle", NULL, },
+      "whoop  doo"
+    },
+    {
+      /* Multiple simple arguments */
+      "space %s %s", { "man", "dances", NULL, },
+      "space man dances"
+    },
+    {
+      /* Literal percent */
+      "100%% of space folk dance", { NULL, },
+      "100% of space folk dance"
+    },
+    {
+      /* Multiple simple arguments */
+      "space %2$s %1$s", { "dances", "man", NULL, },
+      "space man dances"
+    },
+    {
+      /* Skipping an argument (not supported by standard printf) */
+      "space %2$s dances", { "dances", "man", NULL, },
+      "space man dances"
+    },
+
+    /* Failures start here */
+
+    {
+      /* Unsupported conversion */
+      "%x", { "blah", NULL, },
+      NULL
+    },
+    {
+      /* Bad positional argument */
+      "space %55$s dances", { "dances", "man", NULL, },
+      NULL
+    },
+    {
+      /* Zero positional argument */
+      "space %0$s dances", { "dances", "man", NULL, },
+      NULL
+    },
+    {
+      /* Too many args used */
+      "%s %s dances", { "space", NULL, },
+      NULL
+    },
+    {
+      /* Too many digits used */
+      "%1234567890s dances", { "space", NULL, },
+      NULL
+    },
+};
+
+
+static void
+callback (void *data, const char *piece, size_t len)
+{
+    char **str = data;
+    *str = talloc_strndup_append (*str, piece, len);
+}
+
+START_TEST(test_safe_str_printf)
+{
+    const Fixture *fixture;
+    char *out;
+    int num_args;
+    int ret;
+    void *mem_ctx;
+
+    fixture = &fixtures[_i];
+    mem_ctx = talloc_init("safe-printf");
+
+    for (num_args = 0; fixture->args[num_args] != NULL; )
+        num_args++;
+
+    out = talloc_strdup (mem_ctx, "");
+    ret = safe_str_printf_cb(callback, &out, fixture->format, (const char * const*)fixture->args, num_args);
+    if (fixture->result) {
+        ck_assert_int_ge(ret, 0);
+        ck_assert_str_eq(out, fixture->result);
+        ck_assert_int_eq(ret, strlen(out));
+    } else {
+        ck_assert_int_lt(ret, 0);
+    }
+
+    talloc_free(mem_ctx);
+}
+END_TEST
+
+START_TEST(test_safe_str_snprintf)
+{
+    char buffer[8];
+    int ret;
+
+    ret = safe_str_snprintf(buffer, 8, "%s", "space", "man", NULL);
+    ck_assert_int_eq(ret, 5);
+    ck_assert_str_eq(buffer, "space");
+
+    ret = safe_str_snprintf(buffer, 8, "", "space", "man", NULL);
+    ck_assert_int_eq(ret, 0);
+    ck_assert_str_eq(buffer, "");
+
+    ret = safe_str_snprintf(buffer, 8, "the %s %s dances away", "space", "man", NULL);
+    ck_assert_int_eq(ret, 25);
+    ck_assert_str_eq(buffer, "the spa");
+
+    ret = safe_str_snprintf(NULL, 0, "the %s %s dances away", "space", "man", NULL);
+    ck_assert_int_eq(ret, 25);
+
+    ret = safe_str_snprintf(buffer, 8, "%5$s", NULL);
+    ck_assert_int_lt(ret, 0);
+}
+END_TEST
+
+START_TEST(test_safe_str_asprintf)
+{
+    char *buffer;
+    int ret;
+
+    ret = safe_str_asprintf(&buffer, "%s", "space", "man", NULL);
+    ck_assert_int_eq(ret, 5);
+    ck_assert_str_eq(buffer, "space");
+    free (buffer);
+
+    ret = safe_str_asprintf(&buffer, "", "space", "man", NULL);
+    ck_assert_int_eq(ret, 0);
+    ck_assert_str_eq(buffer, "");
+    free (buffer);
+
+    ret = safe_str_asprintf(&buffer, "the %s %s dances away", "space", "man", NULL);
+    ck_assert_int_eq(ret, 25);
+    ck_assert_str_eq(buffer, "the space man dances away");
+    free (buffer);
+
+    ret = safe_str_asprintf(&buffer, "%1$s %1$s %1$s %1$s %1$s %1$s", "space man", NULL);
+    ck_assert_int_eq(ret, 59);
+    ck_assert_str_eq(buffer, "space man space man space man space man space man space man");
+    free (buffer);
+
+    ret = safe_str_asprintf(&buffer, "%5$s", NULL);
+    ck_assert_int_lt(ret, 0);
+}
+END_TEST
+
+static Suite *
+create_safe_printf_suite(void)
+{
+    Suite *s = suite_create("safe-printf");
+    TCase *tc_printf = tcase_create("safe-str-printf");
+
+    /* One for each fixture */
+    tcase_add_loop_test(tc_printf, test_safe_str_printf, 0, (sizeof (fixtures) / sizeof (fixtures[0])));
+
+    tcase_add_test(tc_printf, test_safe_str_snprintf);
+    tcase_add_test(tc_printf, test_safe_str_asprintf);
+
+    suite_add_tcase(s, tc_printf);
+
+    return s;
+}
+
+int
+main(int argc, const char *argv[])
+{
+    int opt;
+    poptContext pc;
+    int failure_count;
+    Suite *suite;
+    SRunner *sr;
+
+    struct poptOption long_options[] = {
+        POPT_AUTOHELP
+        POPT_TABLEEND
+    };
+
+    pc = poptGetContext(argv[0], argc, argv, long_options, 0);
+    while((opt = poptGetNextOpt(pc)) != -1) {
+        switch(opt) {
+        default:
+            fprintf(stderr, "\nInvalid option %s: %s\n\n",
+                    poptBadOption(pc, 0), poptStrerror(opt));
+            poptPrintUsage(pc, stderr, 0);
+            return 1;
+        }
+    }
+    poptFreeContext(pc);
+
+    suite = create_safe_printf_suite();
+    sr = srunner_create(suite);
+    /* If CK_VERBOSITY is set, use that, otherwise it defaults to CK_NORMAL */
+    srunner_run_all(sr, CK_ENV);
+    failure_count = srunner_ntests_failed(sr);
+    srunner_free(sr);
+    return (failure_count==0 ? EXIT_SUCCESS : EXIT_FAILURE);
+}
diff --git a/src/util/safe-printf.c b/src/util/safe-printf.c
new file mode 100644
index 0000000..6c728f0
--- /dev/null
+++ b/src/util/safe-printf.c
@@ -0,0 +1,352 @@
+/*
+ * This file originated in the realmd project
+ *
+ * Copyright 2013 Red Hat Inc
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * See the included COPYING file for more information.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+
+#include "safe-printf.h"
+
+#include <stdarg.h>
+#include <string.h>
+
+#ifndef MIN
+#define MIN(a, b)  (((a) < (b)) ? (a) : (b))
+#endif
+
+#ifndef MAX
+#define MAX(a, b)  (((a) > (b)) ? (a) : (b))
+#endif
+
+static void
+safe_padding (int count,
+              int *total,
+              void (* copy_fn) (void *, const char *, size_t),
+              void *data)
+{
+    char eight[] = "        ";
+    int num;
+
+    while (count > 0) {
+        num = MIN (count, 8);
+        copy_fn (data, eight, num);
+        count -= num;
+        *total += num;
+    }
+}
+
+static void
+dummy_copy_fn (void *data,
+               const char *piece,
+               size_t len)
+{
+
+}
+
+int
+safe_str_printf_cb (void (* copy_fn) (void *, const char *, size_t),
+                    void *data,
+                    const char *format,
+                    const char * const args[],
+                    int num_args)
+{
+    int at_arg = 0;
+    const char *cp;
+    int precision;
+    int width;
+    int len;
+    const char *value;
+    int total;
+    int left;
+    int i;
+
+    if (!copy_fn)
+        copy_fn = dummy_copy_fn;
+
+    total = 0;
+    cp = format;
+
+    while (*cp) {
+
+        /* Piece of raw string */
+        if (*cp != '%') {
+            len = strcspn (cp, "%");
+            copy_fn (data, cp, len);
+            total += len;
+            cp += len;
+            continue;
+        }
+
+        cp++;
+
+        /* An literal percent sign? */
+        if (*cp == '%') {
+            copy_fn (data, "%", 1);
+            total++;
+            cp++;
+            continue;
+        }
+
+        value = NULL;
+        left = 0;
+        precision = -1;
+        width = -1;
+
+        /* Test for positional argument.  */
+        if (*cp >= '0' && *cp <= '9') {
+            /* Look-ahead parsing, otherwise skipped */
+            if (cp[strspn (cp, "0123456789")] == '$') {
+                unsigned int n = 0;
+                for (i = 0; i < 6 && *cp >= '0' && *cp <= '9'; i++, cp++) {
+                    n = 10 * n + (*cp - '0');
+                }
+                /* Positional argument 0 is invalid. */
+                if (n == 0)
+                    return -1;
+                /* Positional argument N too high */
+                if (n > num_args)
+                    return -1;
+                value = args[n - 1];
+                cp++; /* $ */
+            }
+        }
+
+        /* Read the supported flags. */
+        for (; ; cp++) {
+            if (*cp == '-')
+                left = 1;
+            /* Supported but ignored */
+            else if (*cp != ' ')
+                break;
+        }
+
+        /* Parse the width. */
+        if (*cp >= '0' && *cp <= '9') {
+            width = 0;
+            for (i = 0; i < 6 && *cp >= '0' && *cp <= '9'; i++, cp++) {
+                width = 10 * width + (*cp - '0');
+            }
+        }
+
+        /* Parse the precision. */
+        if (*cp == '.') {
+            precision = 0;
+            for (i = 0, cp++; i < 6 && *cp >= '0' && *cp <= '9'; cp++, i++) {
+                precision = 10 * precision + (*cp - '0');
+            }
+        }
+
+        /* Read the conversion character.  */
+        switch (*cp++) {
+        case 's':
+            /* Non-positional argument */
+            if (value == NULL) {
+                /* Too many arguments used */
+                if (at_arg == num_args)
+                    return -1;
+                value = args[at_arg++];
+            }
+            break;
+
+        /* No other conversion characters are supported */
+        default:
+            return -1;
+        }
+
+        /* How many characters are we printing? */
+        len = strlen (value);
+        if (precision >= 0)
+            len = MIN (precision, len);
+
+        /* Do we need padding? */
+        safe_padding (left ? 0 : width - len, &total, copy_fn, data);
+
+        /* The actual data */;
+        copy_fn (data, value, len);
+        total += len;
+
+        /* Do we need padding? */
+        safe_padding (left ? width - len : 0, &total, copy_fn, data);
+    }
+
+    return total;
+}
+
+struct sprintf_ctx {
+    char *data;
+    size_t length;
+    size_t alloc;
+};
+
+static void
+asprintf_copy_fn (void *data,
+                  const char *piece,
+                  size_t length)
+{
+    struct sprintf_ctx *cx = data;
+    void *mem;
+
+    if (!cx->data)
+        return;
+
+    /* Reallocate if necessary */
+    if (cx->length + length + 1 > cx->alloc) {
+        cx->alloc += MAX (length + 1, 1024);
+        mem = realloc (cx->data, cx->alloc);
+        if (mem == NULL) {
+            free (cx->data);
+            cx->data = NULL;
+            return;
+        }
+        cx->data = mem;
+    }
+
+    memcpy (cx->data + cx->length, piece, length);
+    cx->length += length;
+    cx->data[cx->length] = 0;
+}
+
+static const char **
+valist_to_args (va_list va,
+                int *num_args)
+{
+    int alo_args;
+    const char **args;
+    const char *arg;
+    void *mem;
+
+    *num_args = alo_args = 0;
+    args = NULL;
+
+    for (;;) {
+        arg = va_arg (va, const char *);
+        if (arg == NULL)
+            break;
+        if (*num_args == alo_args) {
+            alo_args += 8;
+            mem = realloc (args, sizeof (const char *) * alo_args);
+            if (!mem) {
+                free (args);
+                return NULL;
+            }
+            args = mem;
+        }
+        args[(*num_args)++] = arg;
+    }
+
+    return args;
+}
+
+int
+safe_str_asprintf (char **strp,
+                   const char *format,
+                   ...)
+{
+    struct sprintf_ctx cx;
+    const char **args;
+    int num_args;
+    va_list va;
+    int ret;
+    int i;
+
+    va_start (va, format);
+    args = valist_to_args (va, &num_args);
+    va_end (va);
+
+    if (args == NULL)
+        return -1;
+
+    /* Preallocate a pretty good guess */
+    cx.alloc = strlen (format) + 1;
+    for (i = 0; i < num_args; i++) {
+        cx.alloc += strlen (args[i]);
+    }
+
+    cx.data = malloc (cx.alloc);
+    if (!cx.data) {
+        free (args);
+        return -1;
+    }
+
+    cx.data[0] = '\0';
+    cx.length = 0;
+
+    ret = safe_str_printf_cb (asprintf_copy_fn, &cx, format, args, num_args);
+    if (cx.data == NULL)
+        ret = -1;
+    if (ret < 0)
+        free (cx.data);
+    else
+        *strp = cx.data;
+
+    free (args);
+    return ret;
+}
+
+static void
+snprintf_copy_fn (void *data,
+                  const char *piece,
+                  size_t length)
+{
+    struct sprintf_ctx *cx = data;
+
+    /* Don't copy if too much data */
+    if (cx->length > cx->alloc)
+        length = 0;
+    else if (cx->length + length > cx->alloc)
+        length = cx->alloc - cx->length;
+
+    if (length > 0)
+        memcpy (cx->data + cx->length, piece, length);
+
+    /* Null termination happens later */
+    cx->length += length;
+}
+
+int
+safe_str_snprintf (char *str,
+                   size_t len,
+                   const char *format,
+                   ...)
+{
+    struct sprintf_ctx cx;
+    int num_args;
+    va_list va;
+    const char **args;
+    int ret;
+
+    cx.data = str;
+    cx.length = 0;
+    cx.alloc = len;
+
+    va_start (va, format);
+    args = valist_to_args (va, &num_args);
+    va_end (va);
+
+    if (args == NULL)
+        return -1;
+
+    if (len)
+        cx.data[0] = '\0';
+
+    ret = safe_str_printf_cb (snprintf_copy_fn, &cx, format, args, num_args);
+    if (ret < 0)
+        return ret;
+
+    /* Null terminate appropriately */
+    if (len > 0)
+        cx.data[MIN(cx.length, len - 1)] = '\0';
+
+    free (args);
+    return ret;
+}
diff --git a/src/util/safe-printf.h b/src/util/safe-printf.h
new file mode 100644
index 0000000..ad8a687
--- /dev/null
+++ b/src/util/safe-printf.h
@@ -0,0 +1,80 @@
+/*
+ * This file originated in the realmd project
+ *
+ * Copyright 2013 Red Hat Inc
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * See the included COPYING file for more information.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+
+#ifndef __SAFE_PRINTF_H__
+#define __SAFE_PRINTF_H__
+
+#include <stdlib.h>
+
+/*
+ * This is a neutered printf variant that can be used with user-provided
+ * format strings.
+ *
+ * Not only are the normal printf functions not safe to use on user-provided
+ * input (ie: can crash, be abused, etc), they're also very brittle with
+ * regards to positional arguments: one must consume them all or printf will
+ * just abort(). This is because arguments of different sizes are accepted
+ * in the varargs. So obviously the positional code cannot know the offset
+ * of the relevant varargs if some are not consumed (ie: tagged with a
+ * field type).
+ *
+ * Thus the only accepted field type here is 's'. It's all we need.
+ *
+ * In general new code should use a better syntax than printf for configuration
+ * options. This code is here to facilitate robust processing of the
+ * full_name_format syntax we already have, which has been documented as
+ * "printf(3) compatible".
+ *
+ * Features:
+ * - Only string 's' fields are supported
+ * - All the varargs should be strings, followed by a NULL argument
+ * - Both positional '%$1s' and non-positional '%s' are supported
+ * - Field widths '%8s' work as expected
+ * - Precision '%.8s' works, but precision cannot be read from a field
+ * - Left alignment flag is supported '%-8s'.
+ * - The space flag '% 8s' has no effect (it's the default for string fields).
+ * - No more than six digits are supported for widths, precisions, etc.
+ *
+ * Use of other flags or field types will cause the relevant printf call to
+ * return -1. Using too many arguments or incorrect positional arguments
+ * will also cause the call to fail.
+ */
+
+#ifndef GNUC_NULL_TERMINATED
+#if __GNUC__ >= 4
+#define GNUC_NULL_TERMINATED __attribute__((__sentinel__))
+#else
+#define GNUC_NULL_TERMINATED
+#endif
+#endif
+
+int        safe_str_asprintf    (char **strp,
+                                 const char *format,
+                                 ...) GNUC_NULL_TERMINATED;
+
+int        safe_str_snprintf    (char *str,
+                                 size_t len,
+                                 const char *format,
+                                 ...) GNUC_NULL_TERMINATED;
+
+int        safe_str_printf_cb   (void (* callback) (void *data, const char *piece, size_t len),
+                                 void *data,
+                                 const char *format,
+                                 const char * const args[],
+                                 int num_args);
+
+#endif /* __SAFE_PRINTF_H__ */
-- 
1.8.4.2

