From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-03-25T11:01:15+00:00 Subject: [ruby-core:125129] [Ruby Feature#21963] A solution to completely avoid allocated-but-uninitialized objects Issue #21963 has been updated by Eregon (Benoit Daloze). @jhawthorn That's a good point, thank you. I reread https://bugs.ruby-lang.org/issues/21267 and back then I also wanted to have a way for safe initialization but didn't look yet at how to achieve it. First I think this proposal still has value because it ensures that `initialize/initialize_dup/initialize_clone` are called after allocation, and that's wasn't the case before (because the user could just call `Class#allocate` and never follow with `initialize*`). Indeed, `initialize/initialize_clone/initialize_dup` can still be overwritten to produce a logically-broken object, that is already the case today. Overwriting these methods is effectively breaking the object and it is a bad case of monkey-patching, so I think any exception or different behavior is fair enough there (the user is breaking the object, we cannot prevent that override but they cannot expect things to work after they broke it), however it must not segfault in that case (I suppose we all agree on that, though I would be tempted to say it's the user's fault but I don't think that will fly). Currently my PR removes the checks so it could segfault. So one way to make progress without introducing segfaults would be to keep those checks. I think that's valuable enough on its own, though not fully satisfying as it keeps these easy-to-forget checks in every instance method. I'd like to avoid those checks, to do that without risking segfaults I think we then need to improve the reliability of initialization and copying for classes defined in C (classes defined in Ruby should not be able to cause a segfault anyway, so that part is not a concern). What if one could provide a `initialization` and `copy` functions/hooks for `TypedData` / `rb_data_type_t`? Then `.new`/`.dup`/`.clone` would call these hooks before `initialize/initialize_clone/initialize_dup`, so we have the guarantee they are always run before handing the object to the user. So we'd have something like: ```c static const rb_data_type_t my_data_type = { ..., .init = my_initialize, // VALUE (*)(int argc, VALUE *argv, VALUE self) .copy = my_init_copy // VALUE (*)(VALUE copy, VALUE original) } ``` The function signatures would match the signatures typically used for `initialize` and `initialize_copy` so it would be easier to share logic with older Ruby versions not having those hooks. One extra complication here is MatchData is not a `TypedData` but a raw `struct RMatch`. Concretely we could redefine `dup` and `clone` on MatchData to achieve the same and call `match_init_copy` before `initialize_dup/initialize_clone` (by reusing `rb_obj_dup_setup`/`rb_obj_clone_setup`). We'd also `rb_undef_alloc_func()` for `MatchData` to make sure `Kernel#dup`/`Kernel#clone` is not used to bypass the initialization logic in the overwritten `dup`/`clone`. `MatchData` doesn't have `initialize` or `new` so we don't need to worry about that one, but if it had we could override `new` to call `match_initialize` before the `initialize` method (e.g. with `rb_obj_call_init_kw`). What do you think? Another idea would be to prevent redefining these crucial hooks (`initialize/initialize_clone/initialize_dup/initialize_copy`) for classes using `Class#safe_initialization`. Preventing override of these methods entirely would be too limitating for subclasses which override the hooks correctly. So instead we could ensure that any override would `super` into the original hook, that would be safe and it could be checked by looking at the AST/bytecode/IR of the overriding method. It might be somewhat complicated if a module is later included and defines e.g. `initialize_copy` but it should be possible to check that it calls `super` too when including in a `safe_initialization` class (directly or indirectly). Preventing monkey-patching in Ruby is unusual, but maybe it would make sense here? Such monkey-patches or overrides which don't call `super` seems inherently broken so maybe we'd only forbid broken defintions which is then a good thing? ---------------------------------------- Feature #21963: A solution to completely avoid allocated-but-uninitialized objects https://bugs.ruby-lang.org/issues/21963#change-116857 * Author: Eregon (Benoit Daloze) * Status: Open ---------------------------------------- A common issue when defining a class is to handle allocated-but-uninitialized objects. For example: ```ruby obj = MyClass.allocate obj.some_method ``` This can easily segfault for classes defined in C and raise an unclear exception for classes defined in Ruby. As a workaround many core (and non-core) classes add a check that they are initialized in *every* instance method. This is suboptimal for performance and correctness, classes should not need to care about allocated-but-uninitialized objects. Fundamentally, to solve this we need to guarantee that after the allocation function is used that either `initialize`, `initialize_dup` or `initialize_clone` is called. And we can't guarantee that for `Class#allocate`. The current workarounds are: * `undef allocate`, but this does not prevent `Class.instance_method(:allocate).bind_call(Foo)`. * `rb_undef_alloc_func()` but this breaks `dup`, `clone` and `Marshal`. The idea is to have in addition of the `public alloc function` (in `rb_classext_struct.as.class.allocator`) an `internal alloc function`. Then: * `Class#new`, `dup`, `clone` and `Marshal` always use the internal alloc function, because they guarantee to call `initialize`, `initialize_dup` or `initialize_clone`. * `rb_define_alloc_func()` sets both fields. * `rb_undef_alloc_func()` sets both fields. * `rb_get_alloc_func()` reads the public alloc function (unchanged) * `Class#allocate` uses the public alloc function (unchanged) We add a new method on `Class`, for example `Class#safe_initialization`, which: * Sets the public alloc function to `UNDEF_ALLOC_FUNC`, same as `rb_undef_alloc_func()`, so `Class#allocate` and `rb_get_alloc_func()` will raise if they are used (as they are unsafe). * Preserves the internal alloc function so `Class#new`, `dup`, `clone` and `Marshal` keep working. After that the class has fully safe intialization and does not need to worry about allocated-but-uninitialized objects anymore. >From https://bugs.ruby-lang.org/issues/21852#note-7 -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/