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

[breaking][core-graphics] Fix unsoundness in CGEventTap API #710

Merged
merged 2 commits into from
Oct 12, 2024

Conversation

tmandry
Copy link
Contributor

@tmandry tmandry commented Sep 29, 2024

The user callback can be dropped while it is still referenced by the run loop. This PR fixes that by invaliding the event tap's mach port which prevents it from sending further events.

The callback_ref field was the wrong type. Of course it should never have been public, no one wants to access the callback to call it (and if they did they would probably segfault). In practice this just meant the "intermediate" box would leak on deallocation.

Both fields have to be made private for the API to remain sound, because their lifetimes are tied together: the port holds a pointer to the callback. Otherwise the mach port can be swapped out and we would invalidate the wrong one. This led to the first actually unfortunate breakage, which is that everyone needs to call .mach_port() now.

The second is that the callback passed to CGEventTap::new() must be 'static because otherwise the CGEventTap struct can be forgotten and the scope it references exited. An alternate CGEventTap::with() API has been added that accepts a non-'static callback.

Below is a test case that segfaults with the existing API and does nothing with the new one.

use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};
use core_graphics::{
    event::{
        CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
        CGEventType, CGMouseButton,
    },
    event_source::CGEventSource,
    event_source::CGEventSourceStateID,
    geometry::CG_ZERO_POINT,
};

fn main() {
    let current = CFRunLoop::get_current();
    match CGEventTap::new(
        CGEventTapLocation::AnnotatedSession,
        CGEventTapPlacement::HeadInsertEventTap,
        CGEventTapOptions::ListenOnly,
        vec![CGEventType::LeftMouseUp],
        |_, _, _| {
            println!("Mouse up");
            CFRunLoop::get_current().stop();
            None
        },
    ) {
        Ok(tap) => unsafe {
            let loop_source = tap.mach_port.create_runloop_source(0).unwrap();
            // let loop_source = tap.mach_port().create_runloop_source(0).unwrap();
            current.add_source(&loop_source, kCFRunLoopCommonModes);
            tap.enable();

            CGEvent::new_mouse_event(
                CGEventSource::new(CGEventSourceStateID::CombinedSessionState).unwrap(),
                CGEventType::LeftMouseUp,
                CG_ZERO_POINT,
                CGMouseButton::Left,
            )
            .unwrap()
            .post(CGEventTapLocation::AnnotatedSession);

            // tap is dropped here
        },
        Err(_) => panic!("Could not create mouse event tap"),
    };

    CFRunLoop::run_current();
}

@tmandry tmandry force-pushed the sound-cgeventtap branch 3 times, most recently from 20bd8dd to 9b32d4a Compare September 29, 2024 07:14
Copy link
Contributor

@madsmtm madsmtm left a comment

Choose a reason for hiding this comment

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

Yeah, this is a unfortunate limitation of the CGEventTapCreate API (that it doesn't allow setting either CFMachPortContext or "shouldFreeInfo" when creating the mach port).

The solution here is completely correct, though I wonder if perhaps we could (ab)use CFMachPortSetInvalidationCallBack to do the deallocation of the closure? This would make it so that we wouldn't have to keep the Box around ourselves, but could instead release it in there - That way, we don't have to force the CFMachPortInvalidate in new (would still be required for with), and we could allow the user to still access the mach_port field directly.

@wusyong
Copy link
Member

wusyong commented Sep 29, 2024

@madsmtm Perhaps it could be future improvement?

We had some publish requests recently. I want to see if we can update some crate versions this week.

@madsmtm
Copy link
Contributor

madsmtm commented Sep 29, 2024

Perhaps it could be future improvement?

Not that much worth in an improvement in the future if we have already forced users to use .mach_port(), the main motivation would be to still allow the old pattern of accessing the mach_port field directly (thereby lessening the breakage of this PR). In any case, I don't have a stake in it.

@waywardmonkeys
Copy link
Contributor

We had some publish requests recently. I want to see if we can update some crate versions this week.

Where?

@wusyong
Copy link
Member

wusyong commented Sep 29, 2024

@waywardmonkeys I mean crates in whole repo. See comments in #706

@tmandry
Copy link
Contributor Author

tmandry commented Sep 29, 2024

The solution here is completely correct, though I wonder if perhaps we could (ab)use CFMachPortSetInvalidationCallBack to do the deallocation of the closure? This would make it so that we wouldn't have to keep the Box around ourselves, but could instead release it in there - That way, we don't have to force the CFMachPortInvalidate in new (would still be required for with), and we could allow the user to still access the mach_port field directly.

You know I actually tried this first, but I don't think it can work. The invalidation callback only receives the info member of the CFMachPortContext, which cannot be set with CGEventTapCreate. I had hoped it would be the same as the refcon passed to CGEventTapCreate, but it isn't.

The other possibility I see is to inject a custom allocator that frees the callback when all its allocated objects have been freed.

Not that much worth in an improvement in the future if we have already forced users to use .mach_port(), the main motivation would be to still allow the old pattern of accessing the mach_port field directly (thereby lessening the breakage of this PR).

Actually, a simpler way of doing this might be to duplicate the mach port field and make one field private. We could invalidate the one in the private field, knowing that it hasn't been tampered with.

@madsmtm
Copy link
Contributor

madsmtm commented Sep 29, 2024

I had hoped it would be the same as the refcon passed to CGEventTapCreate, but it isn't.

Ah, I thought that was the case. Yeah, then there isn't any other way to do this.

@tmandry
Copy link
Contributor Author

tmandry commented Oct 7, 2024

This should be ready to go in then.

@wusyong wusyong added this pull request to the merge queue Oct 12, 2024
Merged via the queue into servo:main with commit 66be81a Oct 12, 2024
7 of 8 checks passed
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 this pull request may close these issues.

4 participants