[ruby-core:126448] [Ruby Bug#22253] Clang UBSan reports NULL/0 memcpy for eval scope locals
Issue #22253 has been reported by yqtian (Yongqiang Tian). ---------------------------------------- Bug #22253: Clang UBSan reports NULL/0 memcpy for eval scope locals https://bugs.ruby-lang.org/issues/22253 * Author: yqtian (Yongqiang Tian) * Status: Open * ruby -v: ruby 4.1.0dev (2026-08-20T02:56:29Z master ed0b427b4b) +PRISM [x86_64-linux] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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 ```text 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: ```sh 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: ```sh UBSAN_OPTIONS=print_stacktrace=0:halt_on_error=0:report_error_type=1 \ ./ruby -e '1.times { _1; eval("") }' ``` Clang reports: ```text 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: ```c 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: ```c if (length > 0) { memcpy(allocated, source, length); } ``` 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. -- https://bugs.ruby-lang.org/
participants (1)
-
yqtian (Yongqiang Tian)