Bug #22253
openClang UBSan reports NULL/0 memcpy for eval scope locals
Description
I found a normal Ruby eval path that passes a null source and zero length to
memcpy in Prism. Clang's nonnull-attribute sanitizer reports the call
because the effective glibc declaration marks the first two memcpy arguments
as nonnull.
I have not observed a crash, output corruption, or an ordinary non-sanitized
failure. I am reporting this as a C correctness/toolchain-contract issue, not a
security issue.
Environment¶
x86-64 Ubuntu
Clang 18.1.3
Ruby revision: ed0b427b4ba3c776c763d2673070e88372b2096a
Relevant sanitizer: -fsanitize=nonnull-attribute
Optimization: -O1
I also checked Ruby HEAD 4012a96bf084d035bf582d58c7dafb31f4762ac6
and the corresponding Prism HEAD
3ea210b30108432b8b75b979872f4fa3c23acb7b; the source site was unchanged.
Reproduction¶
I configured Ruby with Clang and the following flags:
CFLAGS="-fsanitize=nonnull-attribute -fsanitize-recover=all -fno-omit-frame-pointer -g -O1"
LDFLAGS="-fsanitize=nonnull-attribute -fsanitize-recover=all"
Run with the instrumented Ruby:
UBSAN_OPTIONS=print_stacktrace=0:halt_on_error=0:report_error_type=1 \
./ruby -e '1.times { _1; eval("") }'
Clang reports:
prism/prism.c:23015:35: runtime error: null pointer passed as argument 2,
which is declared to never be null
The same report occurs with _1 through _9, binding.eval,
local_variables, and an implicit-it variant.
Cause¶
CRuby constructs the eval scope-local array. A retained slot can have
source == NULL and length == 0. pm_parser_init then executes:
const uint8_t *source = pm_string_source(local);
size_t length = pm_string_length(local);
uint8_t *allocated = (uint8_t *) pm_arena_alloc(&parser->metadata_arena, length, 1);
memcpy(allocated, source, length);
A consumer-side guard removes the report:
However, this crosses the CRuby/Prism integration boundary. An alternative is
for CRuby to compact or initialize every retained eval-scope local before
passing it to Prism.
Is { source = NULL, length = 0 } intended to be accepted as an empty
pm_string_t/scope-local representation here? If so, pm_parser_init could
handle it as a zero-length value. Otherwise, CRuby may need to guarantee a
nonnull source for every retained scope-local entry.
Validation¶
I built unmodified and guarded trees with identical Docker/compiler/configure
flags. The reproducer reports in the unmodified build and is clean in both
recover and halt_on_error=1 modes after adding the guard.
The same native test set passed in both builds, including 2,065 btest cases,
39 eval tests with 537 assertions, and the proc and syntax tests that exercise
this site. The guarded build preserved the behavior of the tested eval paths.
I would be happy to prepare a patch after confirming whether the preferred
invariant belongs in CRuby's eval-scope producer or Prism's consumer.
Updated by mame (Yusuke Endoh) 4 days ago
- Status changed from Open to Assigned
- Assignee set to prism