diff options
author | Stef Walter <stefw@redhat.com> | 2014-01-07 14:44:11 +0100 |
---|---|---|
committer | Jakub Hrozek <jhrozek@redhat.com> | 2014-01-12 15:24:26 +0100 |
commit | b9d8c6172e48a2633ebe196b2e88bebdf9523c20 (patch) | |
tree | 3ea801f4dbba0a859cbe44f8c3e1affe5fb108ba | |
parent | 4b8021779e4db2a212a8214c17e778e843ae2b3a (diff) | |
download | sssd-b9d8c6172e48a2633ebe196b2e88bebdf9523c20.tar.gz sssd-b9d8c6172e48a2633ebe196b2e88bebdf9523c20.tar.xz sssd-b9d8c6172e48a2633ebe196b2e88bebdf9523c20.zip |
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.
-rw-r--r-- | Makefile.am | 16 | ||||
-rw-r--r-- | src/tests/safe-format-tests.c | 244 | ||||
-rw-r--r-- | src/util/safe-format-string.c | 309 | ||||
-rw-r--r-- | src/util/safe-format-string.h | 81 |
4 files changed, 649 insertions, 1 deletions
diff --git a/Makefile.am b/Makefile.am index 2733d4f6d..9c155d68c 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-format-tests if BUILD_SSH non_interactive_check_based_tests += sysdb_ssh-tests @@ -401,6 +402,7 @@ dist_noinst_HEADERS = \ src/util/util.h \ src/util/io.h \ src/util/util_errors.h \ + src/util/safe-format-string.h \ src/util/strtonum.h \ src/util/sss_endian.h \ src/util/sss_nss.h \ @@ -573,6 +575,7 @@ libsss_util_la_SOURCES = \ src/sbus/sssd_dbus_server.c \ src/util/util.c \ src/util/memory.c \ + src/util/safe-format-string.c \ src/util/server.c \ src/util/signal.c \ src/util/usertools.c \ @@ -1180,6 +1183,17 @@ util_tests_LDADD = \ $(SSSD_INTERNAL_LTLIBS) \ libsss_test_common.la +safe_format_tests_SOURCES = \ + src/tests/safe-format-tests.c +safe_format_tests_CFLAGS = \ + $(AM_CFLAGS) \ + $(CHECK_CFLAGS) +safe_format_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-format-tests.c b/src/tests/safe-format-tests.c new file mode 100644 index 000000000..25b917bc0 --- /dev/null +++ b/src/tests/safe-format-tests.c @@ -0,0 +1,244 @@ +/* + * 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-format-string.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_format_string_cb) +{ + 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_format_string_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_format_string) +{ + char buffer[8]; + int ret; + + ret = safe_format_string(buffer, 8, "%s", "space", "man", NULL); + ck_assert_int_eq(ret, 5); + ck_assert_str_eq(buffer, "space"); + + ret = safe_format_string(buffer, 8, "", "space", "man", NULL); + ck_assert_int_eq(ret, 0); + ck_assert_str_eq(buffer, ""); + + ret = safe_format_string(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_format_string(NULL, 0, "the %s %s dances away", "space", "man", NULL); + ck_assert_int_eq(ret, 25); + + ret = safe_format_string(buffer, 8, "%5$s", NULL); + ck_assert_int_lt(ret, 0); +} +END_TEST + +static Suite * +create_safe_format_suite(void) +{ + Suite *s = suite_create("safe-format"); + TCase *tc_format = tcase_create("safe-format-string"); + + /* One for each fixture */ + tcase_add_loop_test(tc_format, test_safe_format_string_cb, 0, + (sizeof (fixtures) / sizeof (fixtures[0]))); + + tcase_add_test(tc_format, test_safe_format_string); + + suite_add_tcase(s, tc_format); + + 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_format_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-format-string.c b/src/util/safe-format-string.c new file mode 100644 index 000000000..11532d42e --- /dev/null +++ b/src/util/safe-format-string.c @@ -0,0 +1,309 @@ +/* + * 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> + */ + +/* + * Some snippets of code from gnulib, but have since been refactored + * to within an inch of their life... + * + * vsprintf with automatic memory allocation. + * Copyright (C) 1999, 2002-2003 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Library General Public License as published + * by the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + */ + +#include "config.h" + +#include "safe-format-string.h" + +#include <errno.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_format_string_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) { + errno = EINVAL; + return -1; + } + /* Positional argument N too high */ + if (n > num_args) { + errno = EINVAL; + 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) { + errno = EINVAL; + return -1; + } + value = args[at_arg++]; + } + break; + + /* No other conversion characters are supported */ + default: + errno = EINVAL; + 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; +} + +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; +} + +struct sprintf_ctx { + char *data; + size_t length; + size_t alloc; +}; + +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_format_string (char *str, + size_t len, + const char *format, + ...) +{ + struct sprintf_ctx cx; + int num_args; + va_list va; + const char **args; + int error = 0; + 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) { + errno = ENOMEM; + return -1; + } + + if (len) + cx.data[0] = '\0'; + + ret = safe_format_string_cb (snprintf_copy_fn, &cx, format, args, num_args); + if (ret < 0) { + error = errno; + } else if (len > 0) { + cx.data[MIN (cx.length, len - 1)] = '\0'; + } + + free (args); + + if (error) + errno = error; + return ret; +} diff --git a/src/util/safe-format-string.h b/src/util/safe-format-string.h new file mode 100644 index 000000000..2f4796de7 --- /dev/null +++ b/src/util/safe-format-string.h @@ -0,0 +1,81 @@ +/* + * 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_FORMAT_STRING_H__ +#define __SAFE_FORMAT_STRING_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 format strings + * 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. + * - Percent signs are to be escaped as usual '%%' + * + * 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. + * + * Functions return -1 on failure and set errno. Otherwise they return + * the full length of the string that would be formatted, with the same + * semantics as snprintf(). + */ + +#ifndef GNUC_NULL_TERMINATED +#if __GNUC__ >= 4 +#define GNUC_NULL_TERMINATED __attribute__((__sentinel__)) +#else +#define GNUC_NULL_TERMINATED +#endif +#endif + +int safe_format_string (char *str, + size_t len, + const char *format, + ...) GNUC_NULL_TERMINATED; + +int safe_format_string_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_FORMAT_STRING_H__ */ |