Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix origin iclass pointer for modules #2978

Merged

Conversation

jeremyevans
Copy link
Contributor

If a module has an origin, and that module is included in another
module or class, previously the iclass created for the module had
an origin pointer to the module's origin instead of the iclass's
origin.

Setting the origin pointer correctly requires using a stack, since
the origin iclass is not created until after the iclass itself.
Implement the stack to track origin pointers and assign them
correctly.

Correctly assigning the origin pointers in the iclass caused a
use-after-free in GC. Fix this by not freeing the method table
for origin iclasses if it is iclass for a module. I think this
is correct because the method table would be owned by the
module itself in that case, but I'm not sure. It is possible
this leads to memory leak, but someone with a better
understanding of the garbage collector should probably determine
that.

This also includes a fix for Module#included_modules to skip
iclasses with origins.

Fixes [Bug #16736]

@jeremyevans jeremyevans force-pushed the fix-origin-iclass-pointer-16736 branch from bac695e to a598884 Compare March 27, 2020 18:33
@jeremyevans jeremyevans requested a review from nobu March 27, 2020 19:30
class.c Outdated
VALUE p, iclass;
int method_changed = 0, constant_changed = 0;

VALUE p, iclass, orig_iclass;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class.c:935:22: warning: unused variable 'orig_iclass' [-Wunused-variable]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll fix this.

class.c Outdated
add_subclass = TRUE;
if (module != RCLASS_ORIGIN(module)) {
tmp_stack = orig_stack;
orig_stack = malloc(sizeof(struct origin_stack));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this cause potential memory leak, when

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pointer is freed later when the origin iclass is created. However, if there is an exception raised between when this pointer is created and when it is freed, then that would cause a small memory leak (12-24 bytes depending on arch * number of items in stack). I do not know if any of the functions in thewhile loop can raise an exception, but it's complex enough that I cannot rule it out. Should I switch this to use rb_alloc_tmp_buffer2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured out a better approach, by storing the information in a hidden Ruby array. This should only increase memory by 8-16 bytes per module origin iclass. Assuming it passes CI, I'll commit it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this loop, rb_include_class_new, rb_module_add_to_subclasses_list, rb_module_add_to_subclasses_list and add_refined_method_entry_i can cause NoMemoryError.
How about pushing iclass and origin to a hidden array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already implemented in the latest commit. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean pushing to a single array instead of chaining.

        add_subclass = TRUE;
        if (module != RCLASS_ORIGIN(module)) {
            if (!origin_stack) origin_stack = rb_ary_tmp_new(2);
            VALUE origin[2] = {iclass, RCLASS_ORIGIN(module)};
            rb_ary_cat(2, origin, origin_stack);
        }
        else if (origin_stack && (origin_len = RARRAY_LEN(ary)) > 1 &&
                 RARRAY_AREF(origin_stack, origin_len - 1) == module) {
            RCLASS_SET_ORIGIN(RARRAY_AREF(origin_stack, (origin_len -= 2)), iclass);
            rb_ary_resize(origin_stack, origin_len);
            add_subclass = FALSE;
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll push another commit that uses that approach.

class.c Outdated
@@ -1128,7 +1153,7 @@ rb_mod_include_p(VALUE mod, VALUE mod2)

Check_Type(mod2, T_MODULE);
for (p = RCLASS_SUPER(mod); p; p = RCLASS_SUPER(p)) {
if (BUILTIN_TYPE(p) == T_ICLASS) {
if (BUILTIN_TYPE(p) == T_ICLASS) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems unnecessary change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, I'll fix this.

@jeremyevans jeremyevans force-pushed the fix-origin-iclass-pointer-16736 branch 6 times, most recently from 2df04cd to ffe1926 Compare April 7, 2020 15:20
@jeremyevans jeremyevans force-pushed the fix-origin-iclass-pointer-16736 branch from ffe1926 to a8cbb89 Compare April 10, 2020 14:04
@jeremyevans
Copy link
Contributor Author

The AppVeyor failures seem unrelated due to merge issues with Git. However, the MinGW failure may be an actual issue (ruby crashes during tests, and at different places in different runs). Unfortuntately, the MinGW failure doesn't provide debugging output, so I'm not sure where to start in regards to determining whether this is an actual issue. Can someone with access to MinGW try this PR and see if it does crash, and if so, provide a backtrace?

@jeremyevans jeremyevans force-pushed the fix-origin-iclass-pointer-16736 branch 3 times, most recently from b5fc6b3 to 4a9a47d Compare April 17, 2020 01:42
@jeremyevans
Copy link
Contributor Author

With the freeing of the iclass method tables being removed, this had a clean CI run. However, I assume this leaks memory and is therefore not acceptable. This is a strong indication there is a double free or use after free on the method table, indicating that we need to change the condition on when to free the method table. However, I'm not sure what the condition should be changed to. I originally thought this issue was specific to Windows, but this CI run also shows a crash on Ubuntu, so maybe it is just more common on Windows. I can't seem to recreate the problem locally on OpenBSD, even with all memory checking turned on.

Previously, iclass method tables were freed only for iclasses that were origins. This changes it to only free iclass method tables if the iclass also represents a class and not a module. Leaving the code as it was previously (always freeing origin iclasses even for module iclasses) would always crash during testing.

@jeremyevans jeremyevans force-pushed the fix-origin-iclass-pointer-16736 branch from 9bf84da to 76c6bfd Compare May 19, 2020 21:16
If a module has an origin, and that module is included in another
module or class, previously the iclass created for the module had
an origin pointer to the module's origin instead of the iclass's
origin.

Setting the origin pointer correctly requires using a stack, since
the origin iclass is not created until after the iclass itself.
Use a hidden ruby array to implement that stack.

Correctly assigning the origin pointers in the iclass caused a
use-after-free in GC.  If a module with an origin is included
in a class, the iclass shares a method table with the module
and the iclass origin shares a method table with module origin.

Mark iclass origin with a flag that notes that even though the
iclass is an origin, it shares a method table, so the method table
should not be garbage collected.  The shared method table will be
garbage collected when the module origin is garbage collected.
I've tested that this does not introduce a memory leak.

This also includes a fix for Module#included_modules to skip
iclasses with origins.

Fixes [Bug #16736]
@jeremyevans jeremyevans force-pushed the fix-origin-iclass-pointer-16736 branch from 76c6bfd to a3a742f Compare May 22, 2020 03:55
@jeremyevans
Copy link
Contributor Author

I've found the bug that was previously causing this to fail. For iclass origins (created when a class/module includes a module with an origin), the method table is shared with the module origin and shouldn't be garbage collected separately. I added a flag to the iclass origin and had the garbage collector skip marking/freeing/moving the method table if the flag is set. This fixes the issue and I checked that it does not introduce a memory leak.

@jeremyevans jeremyevans merged commit c745a60 into ruby:master May 22, 2020
@jeremyevans jeremyevans deleted the fix-origin-iclass-pointer-16736 branch May 23, 2020 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
2 participants