Files
runc/libcontainer/nsenter/escape.c
T
Kir Kolyshkin 928ef7afac libct/nsenter: add json msg escaping
Since the previous commit, some strings logged by write_log() contain a
literal newline, which leads to errors like this one:

> # time="2020-06-07T15:41:37Z" level=error msg="failed to decode \"{\\\"level\\\":\\\"debug\\\", \\\"msg\\\": \\\"nsexec-0[2265]: update /proc/2266/uid_map to '0 1000 1\\n\" to json: invalid character '\\n' in string literal"

The fix is to escape such characters.

Add a simple (as much as it can be) routine which implements JSON string
escaping as required by RFC4627, section 2.5, plus escaping of DEL (0x7f)
character (not required, but allowed by the standard, and usually done
by tools such as jq).

As much as I hate to code something like this, I was not able to find
a ready to consume and decent C implementation (not using glib).

Added a test case (and some additional asserts in C code, conditionally
enabled by the test case) to make sure the implementation is correct.
The test case have to live in a separate directory so we can use
different C flags to compile the test, and use C from go test.

[v2: try to simplify the code, add more tests]
[v3: don't do exit(1), try returning an error instead]

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
2021-04-12 16:47:26 -07:00

143 lines
2.9 KiB
C

#include <stdlib.h>
#include <string.h>
#ifdef ESCAPE_TEST
# include <assert.h>
# define test_assert(arg) assert(arg)
#else
# define test_assert(arg)
#endif
#define DEL '\x7f'
/*
* Poor man version of itoa with base=16 and input number from 0 to 15,
* represented by a char. Converts it to a single hex digit ('0' to 'f').
*/
static char hex(char i)
{
test_assert(i >= 0 && i < 16);
if (i >= 0 && i < 10) {
return '0' + i;
}
if (i >= 10 && i < 16) {
return 'a' + i - 10;
}
return '?';
}
/*
* Given the character, tells how many _extra_ characters are needed
* to JSON-escape it. If 0 is returned, the character does not need to
* be escaped.
*/
static int need_escape(char c)
{
switch (c) {
case '\\':
case '"':
case '\b':
case '\n':
case '\r':
case '\t':
case '\f':
return 1;
case DEL: // -> \u007f
return 5;
default:
if (c > 0 && c < ' ') {
// ASCII decimal 01 to 31 -> \u00xx
return 5;
}
return 0;
}
}
/*
* Escape the string so it can be used as a JSON string (per RFC4627,
* section 2.5 minimal requirements, plus the DEL (0x7f) character).
*
* It is expected that the argument is a string allocated via malloc.
* In case no escaping is needed, the original string is returned as is;
* otherwise, the original string is free'd, and the newly allocated
* escaped string is returned. Thus, in any case, the value returned
* need to be free'd by the caller.
*/
char *escape_json_string(char *s)
{
int i, j, len;
char *c, *out;
/*
* First, check if escaping is at all needed -- if not, we can avoid
* malloc and return the argument as is. While at it, count how much
* extra space is required.
*
* XXX: the counting code must be in sync with the escaping code
* (checked by test_assert()s below).
*/
for (i = j = 0; s[i] != '\0'; i++) {
j += need_escape(s[i]);
}
if (j == 0) {
// nothing to escape
return s;
}
len = i + j + 1;
out = malloc(len);
if (!out) {
free(s);
// As malloc failed, strdup can fail, too, so in the worst case
// scenario NULL will be returned from here.
return strdup("escape_json_string: out of memory");
}
for (c = s, j = 0; *c != '\0'; c++) {
switch (*c) {
case '"':
case '\\':
test_assert(need_escape(*c) == 1);
out[j++] = '\\';
out[j++] = *c;
continue;
}
if ((*c < 0 || *c >= ' ') && (*c != DEL)) {
// no escape needed
test_assert(need_escape(*c) == 0);
out[j++] = *c;
continue;
}
out[j++] = '\\';
switch (*c) {
case '\b':
out[j++] = 'b';
break;
case '\n':
out[j++] = 'n';
break;
case '\r':
out[j++] = 'r';
break;
case '\t':
out[j++] = 't';
break;
case '\f':
out[j++] = 'f';
break;
default:
test_assert(need_escape(*c) == 5);
out[j++] = 'u';
out[j++] = '0';
out[j++] = '0';
out[j++] = hex(*c >> 4);
out[j++] = hex(*c & 0x0f);
}
}
test_assert(j + 1 == len);
out[j] = '\0';
free(s);
return out;
}