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

Exception stack not cleaned up after return inside of a catch handler with a finally clause #34579

Closed
twhitehead opened this issue Jan 29, 2020 · 5 comments · Fixed by #36480
Closed
Assignees
Labels
bug Indicates an unexpected problem or unintended behavior compiler:lowering Syntax lowering (compiler front end, 2nd stage)

Comments

@twhitehead
Copy link
Contributor

twhitehead commented Jan 29, 2020

I ran into issues with the exception test suite failing in a particular build environment when ran in the complete test suite but not by itself.

Through a process of elimination I eventually traced the issue down to test12806 in core.jl leaving things on the exception stack.

The following error.jl code, which is just core test12806 followed by the very first exception test, seems to reliably demonstrate the issue for both 1.3.0 and 1.3.1 across several linux distributions (nix, gento, arch, debian).

using Test
using Base: catch_stack

function test12806()
    let catchb = false, catchc = false, catchr = false, a = []
        for i in 1:3
            try
                throw("try err")
            catch e
                i == 1 && break
                i == 2 && continue
                i == 3 && return (catchb, catchc, catchr, a)
            finally
                i == 1 && (catchb = true; continue)
                i == 2 && (catchc = true; )
                i == 3 && (catchr = true; push!(a, 1))
            end
        end
    end
end
@test test12806() == (true, true, false, [1])
@test length(catch_stack()) == 0

Here is the output I get from running it

$ julia error.jl
Test Failed at /home/tyson/julia/error.jl:22
  Expression: length(catch_stack()) == 0
   Evaluated: 1 == 0
ERROR: LoadError: There was an error during testing
in expression starting at /home/tyson/julia/error.jl:22
caused by [exception 1]
"try err"
Stacktrace:
 [1] test12806() at /home/tyson/julia/error.jl:8
 [2] top-level scope at /home/tyson/julia/error.jl:21
 [3] include_relative(::Module, ::String) at /nix/store/0xhszcg5kaw5kl4z2lyhnwsb2b9k8z27-julia-1.3.0/lib/julia/sys.so:?
 [4] include(::Module, ::String) at /nix/store/0xhszcg5kaw5kl4z2lyhnwsb2b9k8z27-julia-1.3.0/lib/julia/sys.so:?
 [5] exec_options(::Base.JLOptions) at /nix/store/0xhszcg5kaw5kl4z2lyhnwsb2b9k8z27-julia-1.3.0/lib/julia/sys.so:?
 [6] _start() at /nix/store/0xhszcg5kaw5kl4z2lyhnwsb2b9k8z27-julia-1.3.0/lib/julia/sys.so:?
@twhitehead
Copy link
Contributor Author

CCing @JeffBezanson as commit history indicates you added the test case 12806.

@twhitehead
Copy link
Contributor Author

Spent a bit of time trying to simplify this down further. Looks like the actual issue is that the stack doesn't clean up when you do a return inside of a catch block with a finally clause as demonstrated in this error.jl code

using Base: catch_stack

function foo()
  try
    throw("err")
  catch
    return
  finally
  end
end

foo()
println(length(catch_stack()))
$ julia error.jl
1
$

Seems this isn't actually tied to break or continue after all. I'll updated the bug title.

@twhitehead twhitehead changed the title Exception stack not cleaned up after continue/break in exception handler test Exception stack not cleaned up after return inside of a catch handler with a finally clause Feb 18, 2020
@twhitehead
Copy link
Contributor Author

I believe the unique thing in the build environment I've been working in that triggered this (and other) test interaction errors to appear is the fact that it runs isolated from the network.

From looking at the code, I believe the lack of a network causes julia to run the test sequentially one on worker. This means that all tests share some global state and causes issues between each other. I would presume whether these issues occur normally or not will otherwise depend on which tests run on which workers.

I expect you can test for these failures in a non-isolated build environment by manually hacking test/runtests.jl

cd(@__DIR__) do
    n = 1
    if net_on
        n = min(Sys.CPU_THREADS, length(tests))
        n > 1 && addprocs_with_testenv(n)
        LinearAlgebra.BLAS.set_num_threads(1)
    end
    skipped = 0

to not set n>1 (e.g., change net_on to false).

@c42f c42f added the compiler:lowering Syntax lowering (compiler front end, 2nd stage) label Jun 27, 2020
@c42f
Copy link
Member

c42f commented Jun 27, 2020

Very likely this is a bug in lowering.

Syntax desugaring turns the try-catch-finally into the nested form

function foo()
  try
    try
      throw("err")
    catch
      return
    end
  finally
  end
end

which is then transformed by compile-body in a later lowering pass, with some rather tricky logic to handle the possible combinations of try/catch/finally/break/continue/return.

The following form does not have this problem, so it's some interaction with the finally block where returning from within the catch must run the finally code.

function foo()
    try
      throw("err")
    catch
      return
    end
end

@KristofferC KristofferC added the bug Indicates an unexpected problem or unintended behavior label Jun 27, 2020
@c42f
Copy link
Member

c42f commented Jun 30, 2020

I spent a while staring at the applicable part of lowering.

This is a bug in how lowering's linearization pass for exception stack pop_exception interacts with lowering of finally blocks.

Specifically, the lowering of finally is quite subtle when combined with break or return because each finally block may be entered via multiple code paths (eg, different occurrences of return), and these code paths must diverge again once the finally block has completed. To complicate matters further, the return code path must thread through every nested finally block before actually returning, all the while preserving the information about which variable to return.

To get all this to work there's a rather neat system for tagging the return path and emitting the right code to leave the inner scope of any exception handlers, etc. But the code for popping the exception stack is missing (for return-via-finally) or subtly broken (for break-via-finally).

Here's a related failing test for break-via-finally which pops the exception stack too early, so is less serious but still wrong:

function foo()
    while true
        try
            throw("Expected")
        catch
            try
                break
            finally
                @test length(Base.catch_stack()) == 1
            end
        end
    end
end

@c42f c42f assigned c42f and unassigned JeffBezanson Jun 30, 2020
c42f added a commit that referenced this issue Jun 30, 2020
When using `return` or `break` nested inside `finally` handlers,
exception stack lowering failed to pop exceptions from the stack
correctly:

* For `return`, the exception stack was not popped at all. If done
  inside a loop this could eventually cause the runtime to run out of
  memory.
* For `break`, the exception stack was popped too early, causing subtle
  inconsistency in intermediate finally handlers.

Fix these issues by storing the current exception token stack with the
current finally handler information and using it to pop the stack before
jumping into the finally block.

Fixes #34579
c42f added a commit that referenced this issue Jun 30, 2020
When using `return` or `break` nested inside `finally` handlers,
exception stack lowering failed to pop exceptions from the stack
correctly:

* For `return`, the exception stack was not popped at all. If done
  inside a loop this could eventually cause the runtime to run out of
  memory.
* For `break`, the exception stack was popped too early, causing subtle
  inconsistency in intermediate finally handlers.

Fix these issues by storing the current exception token stack with the
current finally handler information and using it to pop the stack before
jumping into the finally block.

Fixes #34579
c42f added a commit that referenced this issue Jul 1, 2020
When using `return` or `break` nested inside `finally` handlers,
exception stack lowering failed to pop exceptions from the stack
correctly:

* For `return`, the exception stack was not popped at all. If done
  inside a loop this could eventually cause the runtime to run out of
  memory.
* For `break`, the exception stack was popped too early, causing subtle
  inconsistency in intermediate finally handlers.

Fix these issues by storing the current exception token stack with the
current finally handler information and using it to pop the stack before
jumping into the finally block.

Fixes #34579
KristofferC pushed a commit that referenced this issue Jul 8, 2020
When using `return` or `break` nested inside `finally` handlers,
exception stack lowering failed to pop exceptions from the stack
correctly:

* For `return`, the exception stack was not popped at all. If done
  inside a loop this could eventually cause the runtime to run out of
  memory.
* For `break`, the exception stack was popped too early, causing subtle
  inconsistency in intermediate finally handlers.

Fix these issues by storing the current exception token stack with the
current finally handler information and using it to pop the stack before
jumping into the finally block.

Fixes #34579

(cherry picked from commit 16ba0dd)
simeonschaub pushed a commit to simeonschaub/julia that referenced this issue Aug 11, 2020
When using `return` or `break` nested inside `finally` handlers,
exception stack lowering failed to pop exceptions from the stack
correctly:

* For `return`, the exception stack was not popped at all. If done
  inside a loop this could eventually cause the runtime to run out of
  memory.
* For `break`, the exception stack was popped too early, causing subtle
  inconsistency in intermediate finally handlers.

Fix these issues by storing the current exception token stack with the
current finally handler information and using it to pop the stack before
jumping into the finally block.

Fixes JuliaLang#34579
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Indicates an unexpected problem or unintended behavior compiler:lowering Syntax lowering (compiler front end, 2nd stage)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants