ruby/eval_error.c
Shugo Maeda 79d4e720f8 Make locals read after longjmp volatile in more setjmp users
Audit of every EC_PUSH_TAG user, following the LLVM 19 longjmp
rematerialization bug worked around in rb_fiber_start: locals and
arguments that are read on the longjmp path but not modified inside
the tag region are made volatile.  C99 7.13.2.1p3 guarantees such
unmodified locals, but LLVM 17 and 19 have been observed to
reconstruct their values incorrectly after a longjmp (cfp in
rb_iterate0, fiber in rb_fiber_start).  volatile on a parameter may
not be effective (67d0f4821f,
https://bugs.ruby-lang.org/issues/19145), so arguments are copied
into volatile locals instead.

* vm_eval.c (rb_iterate0): cfp is compared against the escaping
  frame after a longjmp; clang 17 at -O1 with -fPIC was observed to
  clobber it during the development of the ifunc invalidation for
  https://bugs.ruby-lang.org/issues/22019.  ec is read next to it to
  rewind the frame and to rethrow.

* eval_error.c (rb_ec_error_print_detailed0): opt and highlight are
  read on the path reached by a longjmp from rb_get_backtrace while
  the other locals of the function are already volatile.

* scheduler.c (rb_fiber_scheduler_unblock, fiber_interrupt): ec and
  saved_interrupt_mask are read after a longjmp to restore
  ec->interrupt_mask; cfp next to them was already volatile.

* eval.c (rb_vrescue2): r_proc and data2 are used when the raise is
  handled.  args is left as is; a va_list cannot portably be
  volatile-qualified and is memory-backed in practice.

* eval.c (rb_protect): pstate is written through after the tag is
  popped.

* eval.c (rb_ec_ensure): ec, e_proc and data2 run the ensure proc
  after a longjmp; a clobbered e_proc would miscall it.

* vm_eval.c (vm_catch_protect): stateptr is written through after
  the pop.  tag has its address taken and stays in memory.

* vm_trace.c (exec_hooks_protected): ec and list are passed to
  exec_hooks_postcheck after the pop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 10:37:13 +09:00

589 lines
18 KiB
C

/* -*-c-*- */
/*
* included by eval.c
*/
#define write_warn(str, x) \
(NIL_P(str) ? warn_print(x) : (void)rb_str_cat_cstr(str, x))
#define write_warn2(str, x, l) \
(NIL_P(str) ? warn_print2(x, l) : (void)rb_str_cat(str, x, l))
#define write_warn_enc(str, x, l, enc) \
(NIL_P(str) ? warn_print2(x, l) : (void)rb_enc_str_buf_cat(str, x, l, enc))
#ifdef HAVE_BUILTIN___BUILTIN_CONSTANT_P
#define warn_print(x) RB_GNUC_EXTENSION_BLOCK( \
(__builtin_constant_p(x)) ? \
rb_write_error2((x), (long)strlen(x)) : \
rb_write_error(x) \
)
#else
#define warn_print(x) rb_write_error(x)
#endif
#define warn_print2(x,l) rb_write_error2((x),(l))
#define write_warn_str(str,x) NIL_P(str) ? rb_write_error_str(x) : (void)rb_str_concat((str), (x))
#define warn_print_str(x) rb_write_error_str(x)
static VALUE error_pos_str(void);
static void
error_pos(const VALUE str)
{
VALUE pos = error_pos_str();
if (!NIL_P(pos)) {
write_warn_str(str, pos);
}
}
static VALUE
error_pos_str(void)
{
int sourceline;
VALUE sourcefile = rb_source_location(&sourceline);
if (!NIL_P(sourcefile)) {
ID caller_name;
if (sourceline == 0) {
return rb_sprintf("%"PRIsVALUE": ", sourcefile);
}
else if ((caller_name = rb_frame_callee()) != 0) {
return rb_sprintf("%"PRIsVALUE":%d:in '%"PRIsVALUE"': ",
sourcefile, sourceline,
rb_id2str(caller_name));
}
else {
return rb_sprintf("%"PRIsVALUE":%d: ", sourcefile, sourceline);
}
}
return Qnil;
}
static void
set_backtrace(VALUE info, VALUE bt)
{
ID set_backtrace = rb_intern("set_backtrace");
if (rb_backtrace_p(bt)) {
if (rb_method_basic_definition_p(CLASS_OF(info), set_backtrace)) {
rb_exc_set_backtrace(info, bt);
return;
}
else {
bt = rb_backtrace_to_str_ary(bt);
}
}
rb_check_funcall(info, set_backtrace, 1, &bt);
}
#define CSI_BEGIN "\033["
#define CSI_SGR "m"
static const char underline[] = CSI_BEGIN"1;4"CSI_SGR;
static const char bold[] = CSI_BEGIN"1"CSI_SGR;
static const char reset[] = CSI_BEGIN""CSI_SGR;
static void
print_errinfo(const VALUE eclass, const VALUE errat, const VALUE emesg, const VALUE str, int highlight)
{
long elen = 0;
VALUE mesg;
if (NIL_P(errat) || RARRAY_LEN(errat) == 0 ||
NIL_P(mesg = RARRAY_AREF(errat, 0))) {
error_pos(str);
}
else {
write_warn_str(str, mesg);
write_warn(str, ": ");
}
if (!NIL_P(emesg)) {
elen = RSTRING_LEN(emesg);
}
if (eclass == rb_eRuntimeError && elen == 0) {
if (highlight) write_warn(str, underline);
write_warn(str, "unhandled exception");
if (highlight) write_warn(str, reset);
write_warn(str, "\n");
}
else {
VALUE epath;
epath = rb_class_name(eclass);
if (elen == 0) {
if (highlight) write_warn(str, underline);
write_warn_str(str, epath);
if (highlight) write_warn(str, reset);
write_warn(str, "\n");
}
else {
write_warn_str(str, emesg);
write_warn(str, "\n");
}
}
}
VALUE
rb_decorate_message(const VALUE eclass, VALUE emesg, int highlight)
{
const char *einfo = "";
long elen = 0;
rb_encoding *eenc;
VALUE str = rb_usascii_str_new_cstr("");
if (!NIL_P(emesg) && rb_enc_asciicompat(eenc = rb_enc_get(emesg))) {
einfo = RSTRING_PTR(emesg);
elen = RSTRING_LEN(emesg);
}
else {
eenc = NULL;
}
if (eclass == rb_eRuntimeError && elen == 0) {
if (highlight) write_warn(str, underline);
write_warn(str, "unhandled exception");
if (highlight) write_warn(str, reset);
}
else {
VALUE epath;
epath = rb_class_name(eclass);
if (elen == 0) {
if (highlight) write_warn(str, underline);
write_warn_str(str, epath);
if (highlight) write_warn(str, reset);
}
else {
/* emesg is a String instance */
const char *tail = 0;
if (highlight) write_warn(str, bold);
if (RSTRING_PTR(epath)[0] == '#')
epath = 0;
if ((tail = memchr(einfo, '\n', elen)) != 0) {
write_warn_enc(str, einfo, tail - einfo, eenc);
tail++; /* skip newline */
}
else {
write_warn_str(str, emesg);
}
if (epath) {
write_warn(str, " (");
if (highlight) write_warn(str, underline);
write_warn_str(str, epath);
if (highlight) {
write_warn(str, reset);
write_warn(str, bold);
}
write_warn(str, ")");
if (highlight) write_warn(str, reset);
}
if (tail && einfo+elen > tail) {
if (!highlight) {
write_warn(str, "\n");
write_warn_enc(str, tail, einfo+elen-tail, eenc);
}
else {
elen -= tail - einfo;
einfo = tail;
write_warn(str, "\n");
while (elen > 0) {
tail = memchr(einfo, '\n', elen);
if (!tail || tail > einfo) {
write_warn(str, bold);
write_warn_enc(str, einfo, tail ? tail-einfo : elen, eenc);
write_warn(str, reset);
if (!tail) {
break;
}
}
elen -= tail - einfo;
einfo = tail;
do ++tail; while (tail < einfo+elen && *tail == '\n');
write_warn_enc(str, einfo, tail-einfo, eenc);
elen -= tail - einfo;
einfo = tail;
}
}
}
}
}
RB_GC_GUARD(emesg);
return str;
}
static void
print_backtrace(const VALUE eclass, const VALUE errat, const VALUE str, int reverse, long backtrace_limit)
{
if (!NIL_P(errat)) {
long i;
long len = RARRAY_LEN(errat);
const int threshold = 1000000000;
int width = (len <= 1) ? INT_MIN : ((int)log10((double)(len > threshold ?
((len - 1) / threshold) :
len - 1)) +
(len < threshold ? 0 : 9) + 1);
long skip_start = -1, skip_len = 0;
// skip for stackoverflow
if (eclass == rb_eSysStackError) {
long trace_head = 9;
long trace_tail = 4;
long trace_max = trace_head + trace_tail + 5;
if (len > trace_max) {
skip_start = trace_head;
skip_len = len - trace_max + 5;
}
}
// skip for explicit limit
if (backtrace_limit >= 0 && len > backtrace_limit + 2) {
skip_start = backtrace_limit + 1;
skip_len = len - skip_start;
}
for (i = 1; i < len; i++) {
if (i == skip_start) {
write_warn_str(str, rb_sprintf("\t ... %ld levels...\n", skip_len));
i += skip_len;
if (i >= len) break;
}
VALUE line = RARRAY_AREF(errat, reverse ? len - i : i);
if (RB_TYPE_P(line, T_STRING)) {
VALUE bt = rb_str_new_cstr("\t");
if (reverse) rb_str_catf(bt, "%*ld: ", width, len - i);
write_warn_str(str, rb_str_catf(bt, "from %"PRIsVALUE"\n", line));
}
}
}
}
VALUE rb_get_detailed_message(VALUE exc, VALUE opt);
static int
shown_cause_p(VALUE cause, VALUE *shown_causes)
{
VALUE shown = *shown_causes;
if (!shown) {
*shown_causes = shown = rb_obj_hide(rb_ident_hash_new());
}
if (rb_hash_has_key(shown, cause)) return TRUE;
rb_hash_aset(shown, cause, Qtrue);
return FALSE;
}
static void
add_shown_cause(VALUE cause, VALUE *shown_causes)
{
if (!NIL_OR_UNDEF_P(cause) &&
!THROW_DATA_P(cause) &&
rb_obj_is_kind_of(cause, rb_eException)) {
shown_cause_p(cause, shown_causes);
}
}
static void
show_cause(VALUE errinfo, VALUE str, VALUE opt, VALUE highlight, VALUE reverse, long backtrace_limit, VALUE *shown_causes)
{
VALUE cause = rb_attr_get(errinfo, id_cause);
if (!NIL_P(cause) && rb_obj_is_kind_of(cause, rb_eException) &&
!shown_cause_p(cause, shown_causes)) {
volatile VALUE eclass = CLASS_OF(cause);
VALUE errat = rb_get_backtrace(cause);
VALUE emesg = rb_get_detailed_message(cause, opt);
if (reverse) {
show_cause(cause, str, opt, highlight, reverse, backtrace_limit, shown_causes);
print_backtrace(eclass, errat, str, TRUE, backtrace_limit);
print_errinfo(eclass, errat, emesg, str, RTEST(highlight));
}
else {
print_errinfo(eclass, errat, emesg, str, RTEST(highlight));
print_backtrace(eclass, errat, str, FALSE, backtrace_limit);
show_cause(cause, str, opt, highlight, reverse, backtrace_limit, shown_causes);
}
}
}
void
rb_exc_check_circular_cause(VALUE exc)
{
VALUE cause = exc, shown_causes = 0;
do {
if (shown_cause_p(cause, &shown_causes)) {
rb_raise(rb_eArgError, "circular causes");
}
} while (!NIL_P(cause = rb_attr_get(cause, id_cause)));
}
static void
rb_error_write0(VALUE errinfo, VALUE emesg, VALUE errat, VALUE str, VALUE opt, VALUE highlight, VALUE reverse, VALUE *shown_causes)
{
volatile VALUE eclass;
VALUE local_shown_causes = 0;
long backtrace_limit = rb_backtrace_length_limit;
if (NIL_P(errinfo))
return;
if (!shown_causes) {
shown_causes = &local_shown_causes;
}
if (UNDEF_P(errat)) {
errat = Qnil;
}
eclass = CLASS_OF(errinfo);
if (reverse) {
static const char traceback[] = "Traceback "
"(most recent call last):\n";
const int bold_part = rb_strlen_lit("Traceback");
char buff[sizeof(traceback)+sizeof(bold)+sizeof(reset)-2], *p = buff;
const char *msg = traceback;
long len = sizeof(traceback) - 1;
if (RTEST(highlight)) {
#define APPEND(s, l) (memcpy(p, s, l), p += (l))
APPEND(bold, sizeof(bold)-1);
APPEND(traceback, bold_part);
APPEND(reset, sizeof(reset)-1);
APPEND(traceback + bold_part, sizeof(traceback)-bold_part-1);
#undef APPEND
len = p - (msg = buff);
}
write_warn2(str, msg, len);
show_cause(errinfo, str, opt, highlight, reverse, backtrace_limit, shown_causes);
print_backtrace(eclass, errat, str, TRUE, backtrace_limit);
print_errinfo(eclass, errat, emesg, str, RTEST(highlight));
}
else {
print_errinfo(eclass, errat, emesg, str, RTEST(highlight));
print_backtrace(eclass, errat, str, FALSE, backtrace_limit);
show_cause(errinfo, str, opt, highlight, reverse, backtrace_limit, shown_causes);
}
}
void
rb_error_write(VALUE errinfo, VALUE emesg, VALUE errat, VALUE str, VALUE opt, VALUE highlight, VALUE reverse)
{
rb_error_write0(errinfo, emesg, errat, str, opt, highlight, reverse, NULL);
}
static void
rb_ec_error_print_detailed0(rb_execution_context_t *const ec, const VALUE errinfo, const VALUE str, VALUE emesg0, VALUE *shown_causes)
{
volatile uint8_t raised_flag = ec->raised_flag;
volatile VALUE errat = Qundef;
volatile bool written = false;
volatile VALUE emesg = emesg0;
volatile VALUE opt = rb_hash_new();
volatile VALUE highlight = rb_stderr_tty_p() ? Qtrue : Qfalse;
rb_hash_aset(opt, ID2SYM(rb_intern_const("highlight")), highlight);
if (NIL_P(errinfo))
return;
rb_ec_raised_clear(ec);
EC_PUSH_TAG(ec);
if (EC_EXEC_TAG() == TAG_NONE) {
errat = rb_get_backtrace(errinfo);
}
if (UNDEF_P(emesg)) {
emesg = Qnil;
emesg = rb_get_detailed_message(errinfo, opt);
}
if (!written) {
written = true;
rb_error_write0(errinfo, emesg, errat, str, opt, highlight, Qfalse, shown_causes);
}
EC_POP_TAG();
ec->errinfo = errinfo;
rb_ec_raised_set(ec, raised_flag);
}
static void
rb_ec_error_print_detailed(rb_execution_context_t *const ec, const VALUE errinfo, const VALUE str, VALUE emesg0)
{
rb_ec_error_print_detailed0(ec, errinfo, str, emesg0, NULL);
}
void
rb_ec_error_print(rb_execution_context_t *volatile ec, volatile VALUE errinfo)
{
rb_ec_error_print_detailed(ec, errinfo, Qnil, Qundef);
}
#define undef_mesg_for(v, k) rb_fstring_lit("undefined"v" method '%1$s' for "k" '%2$s'")
#define undef_mesg(v) ( \
is_mod ? \
undef_mesg_for(v, "module") : \
undef_mesg_for(v, "class"))
void
rb_print_undef(VALUE klass, ID id, rb_method_visibility_t visi)
{
const int is_mod = RB_TYPE_P(klass, T_MODULE);
VALUE mesg;
switch (visi & METHOD_VISI_MASK) {
case METHOD_VISI_UNDEF:
case METHOD_VISI_PUBLIC: mesg = undef_mesg(""); break;
case METHOD_VISI_PRIVATE: mesg = undef_mesg(" private"); break;
case METHOD_VISI_PROTECTED: mesg = undef_mesg(" protected"); break;
default: UNREACHABLE;
}
rb_name_err_raise_str(mesg, klass, ID2SYM(id));
}
void
rb_print_undef_str(VALUE klass, VALUE name)
{
const int is_mod = RB_TYPE_P(klass, T_MODULE);
rb_name_err_raise_str(undef_mesg(""), klass, name);
}
#define inaccessible_mesg_for(v, k) rb_fstring_lit("method '%1$s' for "k" '%2$s' is "v)
#define inaccessible_mesg(v) ( \
is_mod ? \
inaccessible_mesg_for(v, "module") : \
inaccessible_mesg_for(v, "class"))
void
rb_print_inaccessible(VALUE klass, ID id, rb_method_visibility_t visi)
{
const int is_mod = RB_TYPE_P(klass, T_MODULE);
VALUE mesg;
switch (visi & METHOD_VISI_MASK) {
case METHOD_VISI_UNDEF:
case METHOD_VISI_PUBLIC: mesg = inaccessible_mesg(""); break;
case METHOD_VISI_PRIVATE: mesg = inaccessible_mesg("private"); break;
case METHOD_VISI_PROTECTED: mesg = inaccessible_mesg("protected"); break;
default: UNREACHABLE;
}
rb_name_err_raise_str(mesg, klass, ID2SYM(id));
}
static int
sysexit_status(VALUE err)
{
VALUE st = rb_ivar_get(err, id_status);
return NUM2INT(st);
}
enum {
EXITING_WITH_MESSAGE = 1,
EXITING_WITH_STATUS = 2,
EXITING_WITH_SIGNAL = 4
};
static int
exiting_split(VALUE errinfo, volatile int *exitcode, volatile int *sigstatus)
{
int ex = EXIT_SUCCESS;
VALUE signo;
int sig = 0;
int result = 0;
if (NIL_P(errinfo)) return 0;
if (THROW_DATA_P(errinfo)) {
int throw_state = ((const struct vm_throw_data *)errinfo)->throw_state;
ex = throw_state & VM_THROW_STATE_MASK;
result |= EXITING_WITH_STATUS;
}
else if (rb_obj_is_kind_of(errinfo, rb_eSystemExit)) {
ex = sysexit_status(errinfo);
result |= EXITING_WITH_STATUS;
}
else if (rb_obj_is_kind_of(errinfo, rb_eSignal)) {
signo = rb_ivar_get(errinfo, id_signo);
sig = FIX2INT(signo);
result |= EXITING_WITH_SIGNAL;
/* no message when exiting by signal */
if (signo == INT2FIX(SIGSEGV) || !rb_obj_is_instance_of(errinfo, rb_eSignal))
/* except for SEGV and subclasses */
result |= EXITING_WITH_MESSAGE;
}
else if (rb_obj_is_kind_of(errinfo, rb_eSystemCallError) &&
FIXNUM_P(signo = rb_attr_get(errinfo, id_signo))) {
sig = FIX2INT(signo);
result |= EXITING_WITH_SIGNAL;
/* no message when exiting by error to be mapped to signal */
}
else {
ex = EXIT_FAILURE;
result |= EXITING_WITH_STATUS | EXITING_WITH_MESSAGE;
}
if (exitcode && (result & EXITING_WITH_STATUS))
*exitcode = ex;
if (sigstatus && (result & EXITING_WITH_SIGNAL))
*sigstatus = sig;
return result;
}
#define unknown_longjmp_status(status) \
rb_bug("Unknown longjmp status %d", status)
static int
error_handle_with_shown_causes(rb_execution_context_t *ec, VALUE errinfo, enum ruby_tag_type ex, VALUE *shown_causes)
{
int status = EXIT_FAILURE;
if (rb_ec_set_raised(ec))
return EXIT_FAILURE;
switch (ex & TAG_MASK) {
case 0:
status = EXIT_SUCCESS;
break;
case TAG_RETURN:
error_pos(Qnil);
warn_print("unexpected return\n");
break;
case TAG_NEXT:
error_pos(Qnil);
warn_print("unexpected next\n");
break;
case TAG_BREAK:
error_pos(Qnil);
warn_print("unexpected break\n");
break;
case TAG_REDO:
error_pos(Qnil);
warn_print("unexpected redo\n");
break;
case TAG_RETRY:
error_pos(Qnil);
warn_print("retry outside of rescue clause\n");
break;
case TAG_THROW:
/* TODO: fix me */
error_pos(Qnil);
warn_print("unexpected throw\n");
break;
case TAG_RAISE:
if (!(exiting_split(errinfo, &status, NULL) & EXITING_WITH_MESSAGE)) {
break;
}
/* fallthrough */
case TAG_FATAL:
rb_ec_error_print_detailed0(ec, errinfo, Qnil, Qundef, shown_causes);
break;
default:
unknown_longjmp_status(ex);
break;
}
rb_ec_reset_raised(ec);
return status;
}
static int
error_handle(rb_execution_context_t *ec, VALUE errinfo, enum ruby_tag_type ex)
{
return error_handle_with_shown_causes(ec, errinfo, ex, NULL);
}