Bug #22254
open[Prism] Clang UBSan reports NULL/0 memcpy while serializing empty strings
Description
I found a public Prism.dump path that passes a null source and zero length to
memcpy in Prism's buffer helper. 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¶
From a Ruby checkout, create an out-of-tree build with the same flags used in the validation:
./autogen.sh
mkdir build-ubsan
cd build-ubsan
../configure \
CC=clang-18 \
CXX=clang++-18 \
CFLAGS="-fsanitize=nonnull-attribute,returns-nonnull-attribute,nullability-arg,nullability-return,nullability-assign -fsanitize-recover=all -fno-omit-frame-pointer -g -O1" \
CXXFLAGS="-fsanitize=nonnull-attribute,returns-nonnull-attribute,nullability-arg,nullability-return,nullability-assign -fsanitize-recover=all -fno-omit-frame-pointer -g -O1" \
LDFLAGS="-fsanitize=nonnull-attribute,returns-nonnull-attribute,nullability-arg,nullability-return,nullability-assign -fsanitize-recover=all" \
optflags="-O1" \
debugflags="-g" \
--disable-install-doc \
--with-baseruby=/usr/bin/ruby
make -j4
UBSAN_OPTIONS=print_stacktrace=0:halt_on_error=0:report_error_type=1 \
ruby ../tool/runruby.rb -- -rprism -e 'Prism.dump("//")'
Clang reports:
prism/buffer.c:86:40: runtime error: null pointer passed as argument 2,
which is declared to never be null
Other reproducing inputs include %r{}, //im, and an empty quoted symbol. Nonempty regexps and symbols, an ordinary empty string, and %q[] were clean controls.
Cause¶
Some Prism node fields use a null-backed empty string representation.
pm_serialize_string passes the source and zero length through
pm_buffer_append_bytes to:
static PRISM_INLINE void
pm_buffer_append(pm_buffer_t *buffer, const void *source, size_t length) {
size_t cursor = buffer->length;
if (pm_buffer_append_length(buffer, length)) {
memcpy(buffer->value + cursor, source, length);
}
}
A sink-side repair is:
if (pm_buffer_append_length(buffer, length)) {
if (length > 0) {
memcpy(buffer->value + cursor, source, length);
}
}
Another possibility is to skip pm_buffer_append_bytes when length == 0 in
the serialization template, preserving a stricter buffer-helper invariant.
Should the buffer append helpers define a zero-length append as a no-op even
when the source is null, or should the serialization caller filter null-backed
empty strings?
Validation¶
I built unmodified and guarded trees with identical Docker/compiler/configure
flags. The reproducing Prism.dump inputs report in the unmodified build and
are clean in both recover and halt_on_error=1 modes after adding the buffer
guard.
The unmodified and guarded builds produced byte-identical output for
Prism.dump("//"):
The same native test set passed in both builds, including 1,003 Prism dump tests
with 403,474 assertions, plus Prism freeze/parse and Ruby eval/proc/syntax
tests. The guarded build produced no buffer.c report in this test set.
I would be happy to prepare a patch after confirming whether the preferred
repair belongs in the common buffer helper or the serialization template.
Updated by mame (Yusuke Endoh) 3 days ago
- Status changed from Open to Assigned
- Assignee set to prism