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

Object.lower() can result in use-after-free errors #1797

Closed
bendk opened this issue Oct 22, 2023 · 3 comments · Fixed by #1879 · May be fixed by #1823
Closed

Object.lower() can result in use-after-free errors #1797

bendk opened this issue Oct 22, 2023 · 3 comments · Fixed by #1879 · May be fixed by #1823
Assignees

Comments

@bendk
Copy link
Contributor

bendk commented Oct 22, 2023

When an object is lowered from a foreign language we return the raw pointer from the foreign code then clone the Arc using the raw pointer in the Rust code.

How do we know that the pointer is still valid in that Rust code? For normal Rust calls, we can be sure since the a reference to the foreign object is still on the stack. However, for callbacks there are cases when this isn't true:

  • Thread A returns the raw pointer, then gets interrupted (after the Python function returns, but before the Rust code runs).
  • Thread B destroys the Rust object
  • Thread A continues and calls from_raw on the freed raw pointer.

I also think there's a related case when Rust returns a callback interface or trait interface.

@bendk bendk self-assigned this Oct 22, 2023
bendk added a commit to bendk/uniffi-rs that referenced this issue Oct 26, 2023
Changed the FFI for lowering objects avoid the scenario layed out in the
issue.  The new system is that the foreign code calls `inc_ref` on the
handle, then the Rust code calls `remove` on it.  This effectively
makes it so `lower` returns an owned handle instead of a borrowed one.

This replaces a lot of Kotlin code that was concerned about the same
issue in a similar situation.  Removed the `test_handlerace.kts` test
for this, I believe this is now all handled at a lower level and we
don't need this test.  FWIW, the test was failing, but just because we
now raise a different exception -- and the new exception mentions that you
may be using the handle after it's free.
bendk added a commit to bendk/uniffi-rs that referenced this issue Oct 26, 2023
Changed the FFI for lowering objects avoid the scenario layed out in the
issue.  The new system is that the foreign code calls `inc_ref` on the
handle, then the Rust code calls `remove` on it.  This effectively
makes it so `lower` returns an owned handle instead of a borrowed one.

This replaces a lot of Kotlin code that was concerned about the same
issue in a similar situation.  Removed the `test_handlerace.kts` test
for this, I believe this is now all handled at a lower level and we
don't need this test.  FWIW, the test was failing, but just because we
now raise a different exception -- and the new exception mentions that you
may be using the handle after it's free.
bendk added a commit to bendk/uniffi-rs that referenced this issue Oct 26, 2023
Changed the FFI for lowering objects avoid the scenario layed out in the
issue.  The new system is that the foreign code calls `inc_ref` on the
handle, then the Rust code calls `remove` on it.  This effectively
makes it so `lower` returns an owned handle instead of a borrowed one.

This replaces a lot of Kotlin code that was concerned about the same
issue in a similar situation.  Removed the `test_handlerace.kts` test
for this, I believe this is now all handled at a lower level and we
don't need this test.  FWIW, the test was failing, but just because we
now raise a different exception -- and the new exception mentions that you
may be using the handle after it's free.
bendk added a commit to bendk/uniffi-rs that referenced this issue Oct 27, 2023
Changed the FFI for lowering objects avoid the scenario layed out in the
issue.  The new system is that the foreign code calls `inc_ref` on the
handle, then the Rust code calls `remove` on it.  This effectively
makes it so `lower` returns an owned handle instead of a borrowed one.

This replaces a lot of Kotlin code that was concerned about the same
issue in a similar situation.  Removed the `test_handlerace.kts` test
for this, I believe this is now all handled at a lower level and we
don't need this test.  FWIW, the test was failing, but just because we
now raise a different exception -- and the new exception mentions that you
may be using the handle after it's free.
bendk added a commit to bendk/uniffi-rs that referenced this issue Oct 27, 2023
Changed the FFI for lowering objects avoid the scenario layed out in the
issue.  The new system is that the foreign code calls `inc_ref` on the
handle, then the Rust code calls `remove` on it.  This effectively
makes it so `lower` returns an owned handle instead of a borrowed one.

This replaces a lot of Kotlin code that was concerned about the same
issue in a similar situation.  Removed the `test_handlerace.kts` test
for this, I believe this is now all handled at a lower level and we
don't need this test.  FWIW, the test was failing, but just because we
now raise a different exception -- and the new exception mentions that you
may be using the handle after it's free.
bendk added a commit to bendk/uniffi-rs that referenced this issue Oct 27, 2023
Changed the FFI for lowering objects avoid the scenario layed out in the
issue.  The new system is that the foreign code calls `inc_ref` on the
handle, then the Rust code calls `remove` on it.  This effectively
makes it so `lower` returns an owned handle instead of a borrowed one.

This replaces a lot of Kotlin code that was concerned about the same
issue in a similar situation.  Removed the `test_handlerace.kts` test
for this, I believe this is now all handled at a lower level and we
don't need this test.  FWIW, the test was failing, but just because we
now raise a different exception -- and the new exception mentions that you
may be using the handle after it's free.
bendk added a commit to bendk/uniffi-rs that referenced this issue Oct 27, 2023
Changed the FFI for lowering objects avoid the scenario layed out in the
issue.  The new system is that the foreign code calls `inc_ref` on the
handle, then the Rust code calls `remove` on it.  This effectively
makes it so `lower` returns an owned handle instead of a borrowed one.

This replaces a lot of Kotlin code that was concerned about the same
issue in a similar situation.  Removed the `test_handlerace.kts` test
for this, I believe this is now all handled at a lower level and we
don't need this test.  FWIW, the test was failing, but just because we
now raise a different exception -- and the new exception mentions that you
may be using the handle after it's free.
@bendk
Copy link
Contributor Author

bendk commented Oct 31, 2023

I think the general system we should use is that object handles are passed as borrows and returned as owned values. In this case, we should be cloning the handle before returning it.

This seems pretty intuitive to me. In Rust you can pass a reference in without any special handling, but returning a reference requires lifetime checks. You can't check the lifetime of an object on the other side of the FFI, so the rule has to be any handle that's returned is an owned value.

@bendk bendk mentioned this issue Nov 1, 2023
@bendk
Copy link
Contributor Author

bendk commented Nov 1, 2023

I think the above comment represents the ideal solution, but implementing that at this point would be quite complex. We could split lower() into lower_arg() and lower_return() in the foreign bindings, like the Rust code does. But then what about container objects that get lowered into Rust buffers? I think we would need to split write into write_arg and write_return which just feels like too much work to me.

I'm going to go with the simpler solution: always clone the reference in lower(). It's an extra FFI call in some circumstances, but I think we can live with that.

bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 1, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 1, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 1, 2023
Now object handles are always cloned before they're lowered, then
consumed on the Rust side.  This makes it so `lower` returns an owned
handle instead of a borrowed one.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 1, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
@bendk bendk mentioned this issue Nov 1, 2023
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 1, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 1, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 2, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 2, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 2, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 2, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 2, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 2, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
@arg0d
Copy link
Contributor

arg0d commented Nov 8, 2023

I'm trying to understand your proposed solution.

What would be the ownership of handles in case of tail-returning foreign handle? If foreign bindings transfer ownership of foreign handle to Rust, then its up to Rust to free the foreign handle. If foreign bindings borrow foreign handle to Rust, then Rust doesn't have to free the handle, and foreign bindings will free the handle after function call. However, borrowing means the handle will go out of scope as soon as Rust function exits, so the handle can't be retained for any longer for callback-like use cases. How does cloning the handle in lower() solve the issue here?

123 = lower(PyButton())

123 = press(123) {
    // Rust
    foreign.free_handle(123)
    return 123
}

button = lift(123) // error: invalid 123

To me it sounds like what is actually needed here is to dedicate one handle bit to signal that its a tail-handle, and should be freed when lifting by foreign bindings themselves. This would create an option for Rust to conditionally either free the handle in the more usual case, or omit freeing the handle and instead set the tail bit for the handle. I'm not entirely sure how practical it would be detect this type of tail-return.

bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 9, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 9, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 9, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Nov 9, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 1, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 1, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 1, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 1, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 1, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 1, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.

Removed the reexport-scaffolding-macro fixture which broke and often
requires changes when the FFI changes.  I don't think it's giving us
enough value at this point to justify continuing to update it.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 4, 2023
I want to fix mozilla#1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit that referenced this issue Dec 4, 2023
I want to fix #1797 by cloning the object handle in lower().  However,
this presents an issue in the dynamic languages because they perform
various failable checks in lower(): type checks, bounds checks, etc.  If
the check fails, then the cloned handle will be leaked.

Prevent this by adding a `check()` method.  We do the check first then
once we get to `lower()`, nothing can fail.
bendk added a commit to bendk/uniffi-rs that referenced this issue Dec 4, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.

Removed the reexport-scaffolding-macro fixture which broke and often
requires changes when the FFI changes.  I don't think it's giving us
enough value at this point to justify continuing to update it.
bendk added a commit that referenced this issue Dec 4, 2023
Currently, `lower()` always returns a borrow of the object handle.  This
is fine for function arguments, since you know the object is still
alive on the stack while the function is being called.  However, for
function returns this is not correct.

To fix this: clone the handle in `lower()`. Added a test for this -- it
was surprisingly easy to cause a segfault with the current behavior.

Removed the reexport-scaffolding-macro fixture which broke and often
requires changes when the FFI changes.  I don't think it's giving us
enough value at this point to justify continuing to update it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants