Bug #22324
closedPure attributes allow Clang to remove observable C API effects
Description
Several public C APIs are marked RBIMPL_ATTR_PURE() even though their
documented behavior includes observable effects. Clang can consequently remove
calls when an extension discards the return value.
I reproduced three cases against current Ruby at e60ce574f0:
rb_intern_const()creates and permanently registers a symbol. When the
return value is discarded, Clang removes the call and the symbol does not
appear inSymbol.all_symbols.rb_class_inherited_p()can raise the documentedTypeError. When the
return value is discarded, Clang removes the call and execution continues.rb_class_superclass()can raiseTypeErrorfor an uninitialized class.
That discarded call is also removed.
The relevant extension functions are:
static VALUE probe_intern_discard(VALUE self, VALUE name)
{
rb_intern_const(StringValueCStr(name));
return Qnil;
}
static VALUE probe_inherited_discard(VALUE self)
{
rb_class_inherited_p(rb_cObject, Qnil);
return ID2SYM(rb_intern("continued"));
}
static VALUE probe_superclass_discard(VALUE self)
{
VALUE uninitialized_class = rb_obj_alloc(rb_cClass);
rb_class_superclass(uninitialized_class);
return ID2SYM(rb_intern("continued"));
}
The test uses a fresh generated symbol name and checks Symbol.all_symbols
before and after the first call. It also catches TypeError around the two
class calls. With the current headers and Clang 18, I observed:
intern_before=false intern_after=false expected_after=true
inherited_discard=continued expected=type_error
superclass_discard=continued expected=type_error
These removals occur even in the tested -O0 extension build because the
header explicitly tells the compiler that the calls are pure.
A possible fix is to remove RBIMPL_ATTR_PURE() from:
I did not remove the separate noalias annotation from rb_intern_const(),
because it is not needed to reproduce this problem.
After removing only the three pure attributes, the same extension reports:
intern_before=false intern_after=true expected_after=true
inherited_discard=type_error expected=type_error
superclass_discard=type_error expected=type_error
The isolated change leaves the independent typed-data null probe unchanged,
and the complete patched Ruby tree passes make test.
Should these symbol-registration and documented exception effects be preserved
when callers discard the return values? If so, is removing pure from these
three declarations the preferred correction, or is there a narrower contract
Ruby would prefer for any of them?