Remove the `fixed_stack_segment` attribute #10155

Closed
alexcrichton opened this Issue Oct 29, 2013 · 11 comments

Comments

Projects
None yet
5 participants
@alexcrichton
Member

alexcrichton commented Oct 29, 2013

In today's meeting we have decided to jettison segmented stacks. This means that there's no need for this attribute because the purpose of it was to lift the stack requirements higher in the callstack to prevent thrashing.

Removing this isn't just as simple as removing it entirely, as we still need to make sure that any FFI call requires a minimum of 2MB of stack remaining. This means that codegen-wise we will have to continue adding the fixed-stack-segment attribute, but the compiler will do this in an automated fashion now instead of requiring users to do it manually.

Additionally, this also means that the externfn! macro should get removed (it was just used to generate the fixed stack segment wrapper).

Nominating for 1.0, this would be awesome to remove.

Note that existing issues had the following problems:

  • Default methods did not get the fixed-stack-segment attribute
  • Methods may not be getting the fixed-stack-segment attribute
@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 29, 2013

Contributor

I don't get it. Why can't we just remove the whole notion of fixed_stack_segment altogether? Why would we generate it at codegen time (what does it even mean?) In any case, how can we possibly guarantee that there is (e.g.) 2MB of stack, when we can't allocate more?

It's true that calling native code can fall off the end of the stack but...well...that's why it's considered unsafe.

Contributor

nikomatsakis commented Oct 29, 2013

I don't get it. Why can't we just remove the whole notion of fixed_stack_segment altogether? Why would we generate it at codegen time (what does it even mean?) In any case, how can we possibly guarantee that there is (e.g.) 2MB of stack, when we can't allocate more?

It's true that calling native code can fall off the end of the stack but...well...that's why it's considered unsafe.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 29, 2013

Contributor

(Also, I should note that guard pages would help us to die a more controlled death here)

Contributor

nikomatsakis commented Oct 29, 2013

(Also, I should note that guard pages would help us to die a more controlled death here)

@alexcrichton

This comment has been minimized.

Show comment
Hide comment
@alexcrichton

alexcrichton Oct 29, 2013

Member

We still need the idea of a large stack segment for when calling FFI. We should at least attempt to make calls to C "safe" by guarnateeing that there's at least 2MB of stack for it to run on (or some large number). The rust function would then request 2MB of space, and if there wasn't that much left it would trigger an overflow (and currently abort).

We'll always suffer from native code falling off the stack, but it's probably true that most realistic native code won't fall off 2 MB of stack.

Member

alexcrichton commented Oct 29, 2013

We still need the idea of a large stack segment for when calling FFI. We should at least attempt to make calls to C "safe" by guarnateeing that there's at least 2MB of stack for it to run on (or some large number). The rust function would then request 2MB of space, and if there wasn't that much left it would trigger an overflow (and currently abort).

We'll always suffer from native code falling off the stack, but it's probably true that most realistic native code won't fall off 2 MB of stack.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 30, 2013

Contributor

I still don't understand what the idea of a large stack segment brings to the table here. It offers no guarantees and will create many false "stack overflow" reports. Worse, there is no way for users to recover -- with stack growth we at least "fell back" to wasting a lot of memory. Without stack growth, you just get an error, even if the C code would have ran fine (which is likely). If it's safety we're after, let's use a guard page instead. Then you only get an error IF your C code actually overflows.

One of the appealing parts of dropping stack growth was dropping all the complexity around FFI calls. If we wind up keeping that complexity, I may want to revisit my opinion about dropping stack growth in the first place. =)

Contributor

nikomatsakis commented Oct 30, 2013

I still don't understand what the idea of a large stack segment brings to the table here. It offers no guarantees and will create many false "stack overflow" reports. Worse, there is no way for users to recover -- with stack growth we at least "fell back" to wasting a lot of memory. Without stack growth, you just get an error, even if the C code would have ran fine (which is likely). If it's safety we're after, let's use a guard page instead. Then you only get an error IF your C code actually overflows.

One of the appealing parts of dropping stack growth was dropping all the complexity around FFI calls. If we wind up keeping that complexity, I may want to revisit my opinion about dropping stack growth in the first place. =)

@alexcrichton

This comment has been minimized.

Show comment
Hide comment
@alexcrichton

alexcrichton Oct 30, 2013

Member

I thought that we were going to keep the __morestack prologue for now in order to avoid having guard pages? We could indeed have guard pages to protect against FFI calls specifically, but then you have the same problem of what if a C stack frame is larger than 4KB (as I imagine is fairly common with LLVM).

Before I made __morestack abort, the maximum stack size was 2MB. This had to change because an FFI call requests 2MB, so it's now 4MB. I've never seen a stack overflow warning related to calling FFI at all, so I don't think that this is very limiting. I also thought that there was only a very light amount of analysis needed to determine whether an FFI function was called or not. Right now it's implemented as just a lint pass (although I haven't looked at the specifics), but it seems like a fairly easy analysis pass to say "does this function call an extern function", so I don't think that we should be worried too much about the complexity.

Member

alexcrichton commented Oct 30, 2013

I thought that we were going to keep the __morestack prologue for now in order to avoid having guard pages? We could indeed have guard pages to protect against FFI calls specifically, but then you have the same problem of what if a C stack frame is larger than 4KB (as I imagine is fairly common with LLVM).

Before I made __morestack abort, the maximum stack size was 2MB. This had to change because an FFI call requests 2MB, so it's now 4MB. I've never seen a stack overflow warning related to calling FFI at all, so I don't think that this is very limiting. I also thought that there was only a very light amount of analysis needed to determine whether an FFI function was called or not. Right now it's implemented as just a lint pass (although I haven't looked at the specifics), but it seems like a fairly easy analysis pass to say "does this function call an extern function", so I don't think that we should be worried too much about the complexity.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 30, 2013

Contributor

The fact that we need 4MB stacks to work around the check suggests to me that it is limiting -- in other words, it's forcing us to allocate a very large stack in order to ensure that we have a very big buffer at the end so we can make native calls, much larger than ought to be necessary.

As far as complexity, I am mostly worried about end-user complexity, not implementation complexity. I agree it's no great challenge to flag those functions which call extern "C" fns, but we can't just uniformly say that they require 2MB of stack. We're going to need the ability to flag fns that require smaller amounts so that they can be used in contexts where users want to explicitly allocate smaller stack frames etc.

Re: morestack vs guard pages, I personally am ok with just having morestack for now, and accepting that calling a native fn can segfault. I'm curious to hear others' opinions though.

Contributor

nikomatsakis commented Oct 30, 2013

The fact that we need 4MB stacks to work around the check suggests to me that it is limiting -- in other words, it's forcing us to allocate a very large stack in order to ensure that we have a very big buffer at the end so we can make native calls, much larger than ought to be necessary.

As far as complexity, I am mostly worried about end-user complexity, not implementation complexity. I agree it's no great challenge to flag those functions which call extern "C" fns, but we can't just uniformly say that they require 2MB of stack. We're going to need the ability to flag fns that require smaller amounts so that they can be used in contexts where users want to explicitly allocate smaller stack frames etc.

Re: morestack vs guard pages, I personally am ok with just having morestack for now, and accepting that calling a native fn can segfault. I'm curious to hear others' opinions though.

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Nov 7, 2013

Member

accepted for P-high. We know what to do after tuesday mtg, just need to do it.

Member

pnkfelix commented Nov 7, 2013

accepted for P-high. We know what to do after tuesday mtg, just need to do it.

@bill-myers

This comment has been minimized.

Show comment
Hide comment
@bill-myers

bill-myers Nov 10, 2013

Contributor

Guard pages are essential (since there's no guarantee the C function will only try to use 4MB or any fixed stack amount) and of course already provided by all OSes since otherwise C code can corrupt the heap by running off the stack, and C compilers obviously insert checks if the stack is larger than 4KB by touching all pages.

However, the check is still necessary if one wants to make the task fail instead of aborting the process on stack overflow, since you can't unwind C code if it causes a stack overflow because it's not guaranteed to be exception safe (and thus you need to unwind before the call).

Contributor

bill-myers commented Nov 10, 2013

Guard pages are essential (since there's no guarantee the C function will only try to use 4MB or any fixed stack amount) and of course already provided by all OSes since otherwise C code can corrupt the heap by running off the stack, and C compilers obviously insert checks if the stack is larger than 4KB by touching all pages.

However, the check is still necessary if one wants to make the task fail instead of aborting the process on stack overflow, since you can't unwind C code if it causes a stack overflow because it's not guaranteed to be exception safe (and thus you need to unwind before the call).

@thestinger

This comment has been minimized.

Show comment
Hide comment
@thestinger

thestinger Nov 10, 2013

Contributor

You can't unwind pure Rust when it hits the stack overflow either, LLVM doesn't consider __morestack to be a place where unwinding could happen.

Contributor

thestinger commented Nov 10, 2013

You can't unwind pure Rust when it hits the stack overflow either, LLVM doesn't consider __morestack to be a place where unwinding could happen.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Nov 11, 2013

Contributor

It seems to me that we should consider adding an LLVM intrinsic for "stack size of current function" or something like that. In that case, we could insert guards ourselves as normal LLVM code. This would also allow us to optimize the placement of stack checks (i.e., move them up the stack chain as far as possible). One complication though is inlining, which can result in redundant checks, though we might be able to teach LLVM to optimize those away as part of GVN/PRE/CSE/whatever-version-of-those-optimizations-LLVM-performs.

One tricky part to optimizing stack checks is that the failure would occur in somewhat unpredictable places to the user, but I think in practice that's ok. (That is, if I have X that (unconditionally) calls Y that (unconditionally) calls Z, and Z would run out of stack, X might be the one to fail.) But whenever you run out of stack there is always a recursive function and in reality one always just looks up the stack at a near infinite series of frames that fail randomly somewhere in the cycle anyhow, so changing the point in the cycle where this occurs is not a big deal.

Contributor

nikomatsakis commented Nov 11, 2013

It seems to me that we should consider adding an LLVM intrinsic for "stack size of current function" or something like that. In that case, we could insert guards ourselves as normal LLVM code. This would also allow us to optimize the placement of stack checks (i.e., move them up the stack chain as far as possible). One complication though is inlining, which can result in redundant checks, though we might be able to teach LLVM to optimize those away as part of GVN/PRE/CSE/whatever-version-of-those-optimizations-LLVM-performs.

One tricky part to optimizing stack checks is that the failure would occur in somewhat unpredictable places to the user, but I think in practice that's ok. (That is, if I have X that (unconditionally) calls Y that (unconditionally) calls Z, and Z would run out of stack, X might be the one to fail.) But whenever you run out of stack there is always a recursive function and in reality one always just looks up the stack at a near infinite series of frames that fail randomly somewhere in the cycle anyhow, so changing the point in the cycle where this occurs is not a big deal.

@thestinger

This comment has been minimized.

Show comment
Hide comment
@thestinger

thestinger Nov 11, 2013

Contributor

It doesn't seem like it's worth keeping around the segmented stack checks if we're not going to treat FFI calls in a special way. Almost all Rust code makes FFI calls, so an infinitely recursive function has a good chance of not being memory safe. It will get close to the boundary and then all it takes is one free call from a unique pointer to blow the stack.

Contributor

thestinger commented Nov 11, 2013

It doesn't seem like it's worth keeping around the segmented stack checks if we're not going to treat FFI calls in a special way. Almost all Rust code makes FFI calls, so an infinitely recursive function has a good chance of not being memory safe. It will get close to the boundary and then all it takes is one free call from a unique pointer to blow the stack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment