frida-go icon indicating copy to clipboard operation
frida-go copied to clipboard

Support cancelling device functions

Open dylanhitt opened this issue 1 year ago • 11 comments

Recently came across a scenario where it would be nice to cancel device functions. For most device funcs this lib seems to fire and forget. For instance Params(). There isn't a way to cancel the C func itself. The best the caller can do here is "cancel" it with go semantics which turns into more of a fire and forget. So like:

func GetParams(fridaDevice frida.DeviceInt, timeoutSecs int) (error, bool) {
	done := make(chan map[string]any, 1)
	errC := make(chan error, 1)

	go func() {
		params, err := fridaDevice.Params()
		if err != nil {
			errC <- err
			return
		}
		done <- params
	}()

	select {
	case <-done:
		fmt.Println("has frida trust")
		return nil, true
	case <-time.After(time.Duration(timeoutSecs) * time.Second):
		return errors.New("timed out waiting for frida params"), false
	case err := <-errC:
		return err, false
	}
}

Setting up everything to support GCancellable is a quite the lift. Same with wrapping every function and with our own CGO and cancelling from within this lib. 🤷

I'm currently trying to figure out a workaround. Just some thoughts while struggling with something. [

dylanhitt avatar Sep 16 '24 20:09 dylanhitt

Hey, this is great idea, but like you said going for GCancellable would be huge thing. We can always start providing functions with context version and over time to cover them all. Besides, every Frida major or minor versions should not have too much functions to implement.

NSEcho avatar Sep 17 '24 05:09 NSEcho

I am thinking whether passing parameter to the functions such as some bool cancellable is easier than creating context versions of functions.

NSEcho avatar Sep 17 '24 09:09 NSEcho

You mind showing a little example of each?

dylanhitt avatar Sep 17 '24 14:09 dylanhitt

context would look something like:

func (d *Device) ParamsCtx(ctx context.Context) (map[string]any, error) {
	cancel := C.g_cancellable_new()

	done := make(chan map[string]any, 1)
	errC := make(chan error, 1)

	go func() {
		params, err := d.params(cancel)
		if err != nil {
			errC <- err
			return
		}
		done <- params
	}()

	select {
	case result := <-done:
		return result, nil
	case <-ctx.Done():
		// cancel code goes here
		return nil, ctx.Err()
	case err := <-errC:
		return nil, err
	}
}

// Params returns system parameters of the device
func (d *Device) Params() (map[string]any, error) {
	return d.params(nil)
}

// Params returns system parameters of the device
func (d *Device) params(cancellable *C.GCancellable) (map[string]any, error) {
	if d.device != nil {
		var err *C.GError
		ht := C.frida_device_query_system_parameters_sync(d.device, cancellable, &err)
		if err != nil {
			return nil, &FError{err}
		}

		params := gHashTableToMap(ht)

		return params, nil
	}
	return nil, errors.New("could not obtain params for nil device")
}

Which is meh. Maybe there's some way that's totally obvious to me where we can make the context timeout code a bit more reusable.

I'm not really following the idea about using a bool.

dylanhitt avatar Sep 17 '24 19:09 dylanhitt

To be honest, I am not quite sure how the GCancellable works so I cannot help that much to choose which one is better.

NSEcho avatar Sep 17 '24 21:09 NSEcho

I'm also learning on the fly here. :swe

My other thought is we support something along these lines.

misc.go Cancellable

type Cancellable struct {
	cancellable *C.GCancellable
}

func NewCancellable() *Cancellable {
	return &Cancellable{
		cancellable: C.g_cancellable_new(),
	}
}

func (c *Cancellable) Cancel() {
	C.g_cancellable_cancel(c.cancellable)
}

device.go d.Params()

// Params returns system parameters of the device
func (d *Device) Params(cancellable *Cancellable) (map[string]any, error) {
	if d.device == nil {
		return nil, errors.New("could not obtain params for nil device")
	}

	var cancel *C.GCancellable
	if cancellable != nil {
		cancel = cancellable.cancellable
	}

	var err *C.GError
	ht := C.frida_device_query_system_parameters_sync(d.device, cancel, &err)
	if err != nil {
		return nil, &FError{err}
	}

	return gHashTableToMap(ht), nil
}

The caller would do something like:

func main() {
	d := frida.NewDeviceManager()
	d.EnumerateDevices()
	fridaDevice, err := d.FindDeviceByID("my device")
	if err != nil {
		log.Fatal(err)
	}
	params, err := fridaDevice.Params(nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(params)
	params, err = fridaDevice.Params(frida.NewCancellable())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(params)
}

We could either break the API with this or make a function called ParamsCancellable. In this case since we're really only asking callers to pass nil when they bump versions as we work through functions where this is needed.

dylanhitt avatar Sep 17 '24 22:09 dylanhitt

Trying this out now. It's actually feels fairly nice. Here's an example of a successful cancel.

func main() {
	d := frida.NewDeviceManager()
	d.EnumerateDevices()
	fridaDevice, err := d.FindDeviceByID("my-device")
	if err != nil {
		log.Fatal(err)
	}
	c := frida.NewCancellable()
	done := make(chan map[string]any, 1)
	errC := make(chan error, 1)

	go func() {
		params, err := fridaDevice.Params(c)
		if err != nil {
			errC <- err
			return
		}
		done <- params
	}()

	c.Cancel()

	select {
	case result := <-done:
		fmt.Println(result)
		return
	case err := <-errC:
		fmt.Println(err)
		return
	}
}

logs

frida-go git:(chore/params-cancel) ✗ go run main.go
FError: Operation was cancelled

dylanhitt avatar Sep 17 '24 23:09 dylanhitt

Hmm this does change users API alot, I think it would be better to provide WithCancel versions of the functions.

NSEcho avatar Sep 18 '24 09:09 NSEcho

What are you thinking exactly. Like:

Func().withcancel(cancellable) Func(withcancel(cancellable)) FuncWithCancel(cancellable)

I believe with versions 1/2 we’d end up needing to support function names with different signatures anyway from what is already there. To be extendable in the future we’d probably want to name functions with this ability

FuncWithOpts

What are your thoughts on the api here?

dylanhitt avatar Sep 18 '24 10:09 dylanhitt

Actually with version 2 using a varadic input we wouldn’t need to change the current signature

dylanhitt avatar Sep 18 '24 11:09 dylanhitt

Through together an example of this:

misc.go

type Cancellable struct {
	cancellable *C.GCancellable
}

func NewCancellable() *Cancellable {
	return &Cancellable{
		cancellable: C.g_cancellable_new(),
	}
}

func (c *Cancellable) Cancel() {
	C.g_cancellable_cancel(c.cancellable)
}

type options struct {
	cancellable *C.GCancellable
}

type OptFunc func(o *options)

func WithCancel(cancel *Cancellable) OptFunc {
	return func(o *options) {
		o.cancellable = cancel.cancellable
	}
}

device.go

// Params returns system parameters of the device
func (d *Device) Params(opts ...OptFunc) (map[string]any, error) {
	o := &options{}
	for _, opt := range opts { // <--- this all can become a helper func
		opt(o)
	}

	return d.params(o)
}

func (d *Device) params(opts *options) (map[string]any, error) {
	if d.device == nil {
		return nil, errors.New("could not obtain params for nil device")
	}

	var err *C.GError
	ht := C.frida_device_query_system_parameters_sync(d.device, opts.cancellable, &err)
	if err != nil {
		return nil, &FError{err}
	}

	return gHashTableToMap(ht), nil
}

implemantion

func main() {
	d := frida.NewDeviceManager()
	d.EnumerateDevices()
	fridaDevice, err := d.FindDeviceByID("my-device")
	if err != nil {
		log.Fatal(err)
	}
	done := make(chan map[string]any, 1)
	errC := make(chan error, 1)

	go func() {
		params, err := fridaDevice.Params()
		if err != nil {
			errC <- err
			return
		}
		done <- params
	}()

	select {
	case result := <-done:
		fmt.Println(result)
		return
	case err := <-errC:
		fmt.Println(err)
		return
	}
}

func main() {
	d := frida.NewDeviceManager()
	d.EnumerateDevices()
	fridaDevice, err := d.FindDeviceByID("my-device")
	if err != nil {
		log.Fatal(err)
	}
	c := frida.NewCancellable()
	done := make(chan map[string]any, 1)
	errC := make(chan error, 1)

	go func() {
		params, err := fridaDevice.Params(frida.WithCancel(c))
		if err != nil {
			errC <- err
			return
		}
		done <- params
	}()

	c.Cancel()

	select {
	case result := <-done:
		fmt.Println(result)
	case err := <-errC:
		fmt.Println(err)
	}
}

we end up sticking with params, err := fridaDevice.Params(). This would also provide plenty of flexibility for any other options moving forward making us resistant to breaking changes.

dylanhitt avatar Sep 18 '24 15:09 dylanhitt

Gonna close. Adding support for context asynchronously. Thanks

dylanhitt avatar Oct 07 '24 16:10 dylanhitt