Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion libpromises/evalfunction.c
Original file line number Diff line number Diff line change
Expand Up @@ -10246,7 +10246,10 @@ void ModuleProtocol(EvalContext *ctx, const char *command, const char *line, int
// context name once it's in the vartable. Maybe this can be relaxed.
sscanf(line + 1, "%256[^=]=", name);

if (CheckID(name))
/* A well-formed line is "%name=<json>". Without the '=' delimiter
* sscanf() leaves the whole tail in name, so strlen(name) == length-1
* and the "length - strlen(name) - 1 - 1" below underflows. */
if (CheckID(name) && line[strlen(name) + 1] == '=')
Comment on lines -10249 to +10252

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your test passes without this change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, you are right. The OOB read only trips under ASan. In a plain build the scan past the line just reads adjacent heap, JsonParse() rejects the garbage, and the variable ends up undefined either way, so the behavioural check could not tell the patched and unpatched paths apart.

Reworked the test to be deterministic instead of leaning on ASan. The "%bad" line now sits with its terminating NUL on the last readable byte before a PROT_NONE guard page, so the underflowed scan walks straight into it. Confirmed both ways locally:

without the fix: Bus error -> test_module_protocol_percent_no_delimiter: Test failed -> 1 out of 4 tests failed
with the fix:    All 4 tests passed

Pushed in 5f94cc1.

{
JsonElement *json = NULL;
Buffer *holder = BufferNewFrom(line+strlen(name)+1+1,
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/evalfunction_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
#include <eval_context.h>
#include <evalfunction.c>

#ifndef __MINGW32__
#include <sys/mman.h>
#if !defined(MAP_ANONYMOUS) && defined(MAP_ANON)
#define MAP_ANONYMOUS MAP_ANON
#endif
#endif

static bool netgroup_more = false;

#if SETNETGRENT_RETURNS_INT
Expand Down Expand Up @@ -126,13 +133,61 @@ static void test_basename(void)
basename_single_testcase("//a//b///c.csv////", ".csv", "c");
}

static void test_module_protocol_percent_no_delimiter(void)
{
EvalContext *ctx = EvalContextNew();
StringSet *tags = StringSetNew();
long persistence = 0;
char context[CF_BUFSIZE] = "test";

/* A well-formed '%name=<json>' line still defines the container, so the
* added delimiter check does not reject valid input. */
char *ok = xstrdup("%good={\"k\":1}");
ModuleProtocol(ctx, "/dev/null", ok, false, context, sizeof(context),
tags, &persistence);
VarRef *good = VarRefParseFromScope("good", context);
assert_true(EvalContextVariableGet(ctx, good, NULL) != NULL);
VarRefDestroy(good);
free(ok);

#ifndef __MINGW32__
/* A '%' line with no '=' makes "length - strlen(name) - 1 - 1" underflow
* to SIZE_MAX and steps the source pointer one past the terminating NUL,
* so BufferAppend() scans off the end of the line buffer. Place the line
* so its NUL is the last readable byte before a PROT_NONE guard page: the
* unpatched code walks into the guard page (crashes), the patched code
* skips the line. */
const size_t pagesize = (size_t) sysconf(_SC_PAGESIZE);
char *region = mmap(NULL, pagesize * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
assert_true(region != MAP_FAILED);
assert_int_equal(mprotect(region + pagesize, pagesize, PROT_NONE), 0);

const char *bad = "%bad";
char *line = region + pagesize - strlen(bad) - 1; /* NUL at page boundary */
memcpy(line, bad, strlen(bad) + 1);

ModuleProtocol(ctx, "/dev/null", line, false, context, sizeof(context),
tags, &persistence);
VarRef *ref = VarRefParseFromScope("bad", context);
assert_true(EvalContextVariableGet(ctx, ref, NULL) == NULL);
VarRefDestroy(ref);

assert_int_equal(munmap(region, pagesize * 2), 0);
#endif

StringSetDestroy(tags);
EvalContextDestroy(ctx);
}

int main()
{
PRINT_TEST_BANNER();
const UnitTest tests[] = {
unit_test(test_hostinnetgroup_found),
unit_test(test_hostinnetgroup_not_found),
unit_test(test_basename),
unit_test(test_module_protocol_percent_no_delimiter),
};

return run_tests(tests);
Expand Down
Loading