-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Fix intermittent SIGBUS on x64 Linux #822
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
Conversation
Thank you for the investigation and the patch. Does following patch work? diff --git i/thread_pthread.c w/thread_pthread.c
index 3ef316c..b7d0bf2 100644
--- i/thread_pthread.c
+++ w/thread_pthread.c
@@ -675,4 +675,7 @@ ruby_init_stack(volatile VALUE *addr
native_main_thread.stack_maxsize = size;
native_main_thread.stack_start = stackaddr;
+# if defined(__linux__) && (defined(__x86_64__) || defined(__i386__))
+ *(volatile unsigned long long *)((char *)stackaddr - size) = 0;
+# endif
return;
} |
It didn't work outside gdb. diff --git a/thread_pthread.c b/thread_pthread.c
index 3ef316c..94b3ef7 100644
--- a/thread_pthread.c
+++ b/thread_pthread.c
@@ -653,6 +653,30 @@ space_size(size_t stack_size)
}
}
+#ifdef __linux__
+static __attribute__((noinline)) void
+reserve_stack(int dir, volatile char *limit, size_t size)
+{
+ volatile char buf[0x100];
+ size -= sizeof(buf); /* margin */
+ if (STACK_GROW_DIRECTION > 0 || dir > 0) {
+ limit += size;
+ if (limit > buf + sizeof(buf)) {
+ size = limit - (buf + sizeof(buf));
+ limit = alloca(size);
+ limit[size-1] = 0;
+ }
+ }
+ else {
+ limit -= size;
+ if (buf > limit) {
+ limit = alloca(buf - limit);
+ limit[0] = 0;
+ }
+ }
+}
+#endif
+
#undef ruby_init_stack
/* Set stack bottom of Ruby implementation.
*
@@ -674,6 +698,9 @@ ruby_init_stack(volatile VALUE *addr
if (get_main_stack(&stackaddr, &size) == 0) {
native_main_thread.stack_maxsize = size;
native_main_thread.stack_start = stackaddr;
+# if defined(__linux__)
+ reserve_stack(STACK_UPPER((VALUE *)(void *)&addr, +1, -1), stackaddr, size);
+# endif
return;
}
} |
@nobu what was wrong with your first patch? Why doesn't that work? |
First patch doesn't work because the Linux kernel appears to check if the access is related to |
This reverts commit 038c7508c51e682983d3b436aa9636e0750e8bb6.
@nobu I've created a new patch, based on your suggested patch. The problem with your patch is that it doesn't check if |
It seems that this patch increases memory usage even in the case it is unnecessary, doesn't it? |
It does, but very slightly. Even though we're fully reserving the entire stack region, only a single additional page is used (the memory that is touched by
This is from my development machine with an 8MB stack. Notice RSS is only 44kb, even though 8MB is reserved. |
The |
Awesome @nobu - would you be able to backport this to 2.2? |
@csfrancis Wow, I spent significant time on this issue and would love to hear some more words how did you run into the issue and how did you approach diagnosing it further and finding a fix. I am also curious about two things:
|
We receive notifications when Ruby processes crash in our production environment (and they dump core).
The core dump for this particular problem looked strange to me for a couple of reasons:
I spent a bit of time investigating various theories, but finally found one that stood out. Given that the target address was valid
I found this to be suspicious because it was 4k aligned. A core file is just a memory dump of a process, it really has a wealth of information. Using
So, what this tells me is that
We can tell the mapping after the heap is the stack, because it starts at My theory at this point was that the stack needed to grow in order to call a function, but couldn't because it was running into the heap. To prove that, I dug into the Linux kernel to figure out what happens when the stack needs to grow. Sure enough, that led me to the URL I pasted in the PR description: http://lxr.free-electrons.com/source/mm/memory.c?v=3.16#L2635. If you look at the function that's being called there, After coming to this conclusion I wrote a proof of concept script. The script calls itself repeatedly, on each execution checking the distance between the heap and stack segments. If they're within a certain threshold, it allocates a large number of Ruby objects, and then calls a recursive function that will force the stack to grow. Because ASLR randomly places the heap and stack segments, this can take a long time to manifest on x64 (it took over 18k executions for me to hit it). However, because i386 has a much smaller virtual address space, I think this problem would occur much faster on that platform.
This could totally affect ordinary C programs if they use a similar workload. What I meant regarding Ruby C stacks growing dynamically is that MRI does not have its own dedicated stack for Ruby code. Because Ruby code and C code share the same stack, if Ruby code calls a C function, it can cause the native stack to grow.
Native threads do have their own stacks. I haven't really investigated how pthreads handle stack allocation, but I suspect they could suffer from the same problem. In our case (Shopify), we're basically using Ruby in a single threaded fashion, so I doubt it would be an issue for us. |
@csfrancis Wow, thank you so much for taking the time to write it up! If I understand you right, isn't this then in the end a flaw in the Linux memory management that perhaps should be reported in the Linux bug tracker? I went through some of the same hops you went, except I discovered this behaviour locally and was fortunate to start already with a semi-reprodicible test case (in the form of a huge app, but still). I ran the test case in valgrind, have seen that it does not crash then, then I ran it in gdb, did not crash either, finally I figured out a way of also reproducing it in gdb by using:
which is quite telling now with your explanation in mind. Anyway, I was able to get a dissasembly and things like that, see my latter comments here: https://bugs.ruby-lang.org/issues/10626 I also validated that the stack pointer is actually aligned and that the stack actually is way smaller than the ulimit -s. I just could not go further than that, and concluded it is most likely related to pthreads - even if you do not use threads at the Ruby level, the Ruby interpreter runs with two pthread threads - one for the timer and one executing the main Ruby thread and pthread's do some of their own magic regarding stack handling (eg. Ruby interpreter does pthread_attr_setstacksize). It would never occur to me a SIGBUS could be triggered in the way you presented, that is just crazy stuff that I don't think is documented anywhere, but of course I think you are right. Having spent so much time on this, respect, man. |
* thread_pthread.c (reserve_stack): fix intermittent SIGBUS on Linux, by reserving the stack virtual address space at process start up so that it will not clash with the heap space. [Fix GH-822] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_2_2@49578 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
* thread_pthread.c (reserve_stack): fix intermittent SIGBUS on Linux, by reserving the stack virtual address space at process start up so that it will not clash with the heap space. [Fix GH-822] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_2_1@50289 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
* thread_pthread.c (reserve_stack): fix intermittent SIGBUS on Linux, by reserving the stack virtual address space at process start up so that it will not clash with the heap space. [Fix rubyGH-822] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@49452 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
@jaroslawr Hello from the future! I don't know if you care about this almost 10 years later, but
Yes, I believe this was a bug in the kernel, and it was fixed in 2017 here: torvalds/linux@c204d21 |
This fixes an intermittent SIGBUS observed on Linux x64. Possibly fixes https://bugs.ruby-lang.org/issues/10626.
The root problem is that if ASLR places the heap and stack regions too close together, the C stack can potentially grow into the heap. The Linux kernel handles this by raising a SIGBUS signal and terminating the process: http://lxr.free-electrons.com/source/mm/memory.c?v=3.16#L2635. Because Ruby C stacks grow dynamically, and Linux process stacks are not reserved/commited up front, it's possible for the heap the prevent the stack from growing to the size allowed by
ulimit
.I have written a small proof of concept that can reproduce the problem on Ruby 2.1.5: https://gist.github.com/csfrancis/46e360d401609275246c
My solution is to fully reserve the stack virtual address space at process start up. I do this by determining the current stack bounds, and then subtracting the available stack space (according to the process'
rlimit
) from the currentrsp
. Touching memory at that value reserves the address range for the stack (but only commits one physical page of memory).Right now, this is Linux
x64only (other platforms will just no-op), but it wouldn't be too difficult to implement the same solution for i386 as well.