From e22ff90646d5e2eac89a286026a19679a0f3c267 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Sun, 18 Oct 2026 22:04:48 -0700 Subject: [PATCH] generate: reject a NUL character inside a JSON string JSON allows \u0000 inside a string, and json-c keeps it, but the parsed value is copied with strdup(), so everything after the first NUL is silently dropped: "options": ["nosuid\u0000,noexec"] parses without any error and yields a single "nosuid" option, with "noexec" gone. The same happens to every string in the document: paths, process arguments and environment, hook paths, annotations. A runtime has no way to notice, as by the time it sees the value it is a perfectly valid, if shorter, C string. Add dup_json_string(), which copies a JSON string, rejecting it if its JSON length does not match its C string length, and use it for every string the generated code and the string maps read. Note that json-c truncates object keys at the first NUL by itself, so those cannot be checked here. Add tests/test-17.c. Reported-in: https://github.com/containers/crun/issues/2105 Signed-off-by: Kir Kolyshkin --- Makefile.am | 3 +- src/ocispec/json_common.c | 36 +++++++++++--- src/ocispec/json_common.h | 2 + src/ocispec/sources.py | 3 +- tests/test-17.c | 101 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 tests/test-17.c diff --git a/Makefile.am b/Makefile.am index 4650b093..de1039c9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -135,7 +135,8 @@ TESTS = tests/test-1 \ tests/test-13 \ tests/test-14 \ tests/test-15 \ - tests/test-16 + tests/test-16 \ + tests/test-17 noinst_PROGRAMS = src/ocispec/validate $(TESTS) diff --git a/src/ocispec/json_common.c b/src/ocispec/json_common.c index b61b07a7..46872d47 100644 --- a/src/ocispec/json_common.c +++ b/src/ocispec/json_common.c @@ -495,6 +495,31 @@ safe_strdup (const char *src) return dst; } +/* Return a copy of the JSON string VAL, or NULL with *ERR set. A JSON string + can contain a NUL character (\u0000) but a C string cannot, so reject it + rather than silently truncating the value at that point. */ +char * +dup_json_string (json_object *val, const char *key, parser_error *err) +{ + const char *str = json_object_get_string (val); + char *ret; + + if (str == NULL) + str = ""; + else if (json_object_is_type (val, json_type_string) + && strlen (str) != (size_t) json_object_get_string_len (val)) + { + if (*err == NULL && asprintf (err, "Invalid NUL character in value for key '%s'", key) < 0) + *err = strdup ("error allocating memory"); + return NULL; + } + + ret = strdup (str); + if (ret == NULL && *err == NULL) + *err = strdup ("error allocating memory"); + return ret; +} + void * safe_malloc (size_t size) { @@ -1243,7 +1268,6 @@ make_json_map_int_string (json_object *src, const struct parser_context *ctx, pa if (srcval != NULL) { - const char *str; if (! json_object_is_type (srcval, json_type_string)) { if (*err == NULL && asprintf (err, "Invalid value with type 'string' for key '%s'", srckey) < 0) @@ -1252,8 +1276,9 @@ make_json_map_int_string (json_object *src, const struct parser_context *ctx, pa } return NULL; } - str = json_object_get_string (srcval); - ret->values[i] = strdup (str ? str : ""); + ret->values[i] = dup_json_string (srcval, srckey, err); + if (ret->values[i] == NULL) + return NULL; } i++; } @@ -1886,7 +1911,6 @@ make_json_map_string_string (json_object *src, const struct parser_context *ctx, } if (srcval != NULL) { - const char *str; if (! json_object_is_type (srcval, json_type_string)) { if (*err == NULL && asprintf (err, "Invalid value with type 'string' for key '%s'", srckey) < 0) @@ -1896,9 +1920,7 @@ make_json_map_string_string (json_object *src, const struct parser_context *ctx, return NULL; } - str = json_object_get_string (srcval); - - ret->values[i] = strdup (str ? str : ""); + ret->values[i] = dup_json_string (srcval, srckey, err); if (ret->values[i] == NULL) { return NULL; diff --git a/src/ocispec/json_common.h b/src/ocispec/json_common.h index a313f214..72e7c335 100644 --- a/src/ocispec/json_common.h +++ b/src/ocispec/json_common.h @@ -128,6 +128,8 @@ json_object *get_val (json_object *tree, const char *name, int type); char *safe_strdup (const char *src); +char *dup_json_string (json_object *val, const char *key, parser_error *err); + void *safe_malloc (size_t size); int common_safe_double (const char *numstr, double *converted); diff --git a/src/ocispec/sources.py b/src/ocispec/sources.py index 391495b8..9a53f6df 100755 --- a/src/ocispec/sources.py +++ b/src/ocispec/sources.py @@ -473,8 +473,7 @@ def emit_read_value(self, c_file, src, dest, keyname, obj_typename, level=1): {json_api.VAL_TYPE} val = {src}; if (val != NULL) {{ - const char *str = {json_api.get_string('val')}; - {dest} = strdup (str ? str : ""); + {dest} = dup_json_string (val, "{keyname}", err); if ({dest} == NULL) return NULL; }} diff --git a/tests/test-17.c b/tests/test-17.c new file mode 100644 index 00000000..8aee1fe8 --- /dev/null +++ b/tests/test-17.c @@ -0,0 +1,101 @@ +/* Copyright (C) 2026 Kir Kolyshkin + +libocispec is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +libocispec 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libocispec. If not, see . + +*/ + +/* Test that a JSON string containing a NUL character is rejected. + + JSON allows \u0000 inside a string, but the parsed value is stored as a C + string, where the first NUL ends it. Unless rejected, the rest of the + value is silently dropped -- a mount option "nosuid\u0000,noexec" used to + become just "nosuid", with no error whatsoever. */ + +#include "config.h" +#include +#include +#include +#include "ocispec/runtime_spec_schema_config_schema.h" + +static const char *const configs[] = { + /* String. */ + "{\"ociVersion\": \"1.0.0\", \"root\": {\"path\": \"rootfs\\u0000/etc\"}}", + + /* String inside an array of objects. */ + "{\"ociVersion\": \"1.0.0\", \"mounts\": [{\"destination\": \"/proc\\u0000/sys\", \"type\": \"proc\"}]}", + + /* Array of strings. */ + "{\"ociVersion\": \"1.0.0\", \"mounts\": [{\"destination\": \"/sys/fs/cgroup\", \"type\": \"cgroup2\"," + " \"options\": [\"nosuid\\u0000,noexec\"]}]}", + + "{\"ociVersion\": \"1.0.0\", \"process\": {\"cwd\": \"/\", \"args\": [\"/bin/sh\", \"-c\\u0000\"]}}", + + /* Map of strings. */ + "{\"ociVersion\": \"1.0.0\", \"annotations\": {\"key\": \"value\\u0000more\"}}", + + /* NUL as the very first character. */ + "{\"ociVersion\": \"1.0.0\", \"hostname\": \"\\u0000\"}", +}; + +int +main () +{ + runtime_spec_schema_config_schema *config = NULL; + parser_error err = NULL; + size_t i; + int ret = 0; + + for (i = 0; i < sizeof (configs) / sizeof (configs[0]); i++) + { + config = runtime_spec_schema_config_schema_parse_data (configs[i], 0, &err); + if (config != NULL) + { + printf ("config #%zu: expected the parse to fail: %s\n", i, configs[i]); + free_runtime_spec_schema_config_schema (config); + free (err); + err = NULL; + ret = 1; + continue; + } + + if (err == NULL) + { + printf ("config #%zu: parse failed without setting an error: %s\n", i, configs[i]); + ret = 1; + continue; + } + + printf ("config #%zu rejected: %s\n", i, err); + free (err); + err = NULL; + } + + /* Other escaped characters are fine. */ + config = runtime_spec_schema_config_schema_parse_data ( + "{\"ociVersion\": \"1.0.0\", \"hostname\": \"a\\u0001b\\u00e9\"}", 0, &err); + if (config == NULL) + { + printf ("valid config rejected: %s\n", err ? err : "(no error)"); + free (err); + return 1; + } + if (strcmp (config->hostname, "a\001b\303\251") != 0) + { + printf ("unexpected hostname: %s\n", config->hostname); + ret = 1; + } + free_runtime_spec_schema_config_schema (config); + + return ret; +}