8000 fix(script): isolate RPC replies and retire cancelled calls by ink-splatters · Pull Request #81 · frida/frida-go · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
GO ?= go
TESTFLAGS ?= -race -count=1 -timeout=120s
GOOS := $(shell $(GO) env GOOS)
RPC_TARGET := frida/testdata/bin/rpc-target

.PHONY: test-integration clean
.DELETE_ON_ERROR:

ifeq ($(filter darwin linux,$(GOOS)),)
test-integration:
@echo "Native RPC tests require macOS or Linux" >&2
@exit 1
else
test-integration: $(RPC_TARGET)
$(GO) test -tags=integration $(TESTFLAGS) ./frida

$(RPC_TARGET): frida/testdata/rpc-target.c
mkdir -p "$(@D)"
$(CC) $(CPPFLAGS) $(CFLAGS) "$<" -o "$@" $(LDFLAGS) $(LDLIBS)
ifeq ($(GOOS),darwin)
/usr/bin/codesign --force --sign - --entitlements frida/testdata/rpc-target.entitlements "$@"

$(RPC_TARGET): frida/testdata/rpc-target.entitlements
endif
endif

clean:
rm -f "$(RPC_TARGET)"
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ Supported OS:
- [x] Android
- [x] Windows

# Tests

With the Frida devkit configured, run `go test ./frida` for unit tests.

Native RPC integration tests run on macOS and Linux:

```sh
make test-integration
```

This builds the C fixture, signs it on macOS, and runs the tests with the race
detector. Fixture compilation uses `CC`, `CPPFLAGS`, `CFLAGS`, `LDFLAGS`, and
`LDLIBS`; `GO` selects the Go executable. Override `TESTFLAGS` to select tests or
change test options. Each test starts and cleans up its own fixture process.
Run `make clean` to remove the built fixture.

# Small example
```golang
package main
Expand Down
2 changes: 1 addition & 1 deletion frida/frida.go
< 7438 td class="blob-num blob-num-expandable" colspan="2"> Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ package frida
#cgo windows LDFLAGS: -lws2_32 -lgdi32 -lole32 -liphlpapi -lsetupapi -lpsapi -lshell32 -lshlwapi -ldnsapi -lcrypt32 -luuid -lshfolder
#cgo CFLAGS: -I/usr/local/include/ -w
#cgo windows CFLAGS: -Wno-error=incompatible-pointer-types
#cgo darwin LDFLAGS: -lbsm -framework IOKit -framework Foundation -framework AppKit -framework Security -lpthread
#cgo darwin LDFLAGS: -lbsm -lresolv -framework IOKit -framework Foundation -framework AppKit -framework Security -lpthread
#cgo darwin CFLAGS: -Wno-error=incompatible-function-pointer-types
#cgo android LDFLAGS: -llog
#cgo android CFLAGS: -DANDROID -Wno-error=incompatible-function-pointer-types
120 changes: 120 additions & 0 deletions frida/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package frida

import (
"context"
"encoding/json"
"sync"

"github.com/google/uuid"
)

type scriptMessageEnvelope struct {
Type string `json:"type"`
Payload []json.RawMessage `json:"payload"`
}

type scriptRPCRequest struct {
ID string
Method string
Args []any
}

type scriptRPCReply struct {
ID string
Value any
}

type pendingScriptRPCCalls struct {
mu sync.Mutex
callsByID map[string]chan any
}

func (p *pendingScriptRPCCalls) register(id string) <-chan any {
p.mu.Lock()
defer p.mu.Unlock()

if p.callsByID == nil {
p.callsByID = make(map[string]chan any)
}
// Each call owns its channel. A reply can finish delivery after cancellation.
reply := make(chan any, 1)
p.callsByID[id] = reply
return reply
}

func (p *pendingScriptRPCCalls) remove(id string) chan any {
p.mu.Lock()
defer p.mu.Unlock()

reply := p.callsByID[id]
delete(p.callsByID, id)
return reply
}

func (p *pendingScriptRPCCalls) complete(reply scriptRPCReply) {
// Removing before delivery gives at most one reply access to the channel.
if channel := p.remove(reply.ID); channel != nil {
channel <- reply.Value
}
}

func (r scriptRPCRequest) MarshalJSON() ([]byte, error) {
args := r.Args
if args == nil {
args = []any{}
}
// Frida's RPC wire format is a positional array, not a JSON object.
return json.Marshal([]any{"frida:rpc", r.ID, "call", r.Method, args})
}

func (s *Script) callExport(ctx context.Context, method string, args []any, post func(context.Context, scriptRPCRequest) error) any {
if ctx.Err() != nil {
return ErrContextCancelled
}
request := scriptRPCRequest{ID: uuid.NewString(), Method: method, Args: args}
reply := s.pendingCalls.register(request.ID)
defer s.pendingCalls.remove(request.ID)
if ctx.Err() != nil {
return ErrContextCancelled
}
if err := post(ctx, request); err != nil {
return err
}
select {
case <-ctx.Done():
return ErrContextCancelled
case value := <-reply:
return value
}
}

// decodeScriptRPCReply 8567 classifies a native message before decoding its reply.
// isRPC remains true for malformed reserved messages; a nil reply discards them.
func decodeScriptRPCReply(message string) (reply *scriptRPCReply, isRPC bool) {
var envelope scriptMessageEnvelope
if json.Unmarshal([]byte(message), &envelope) != nil || envelope.Type != "send" || len(envelope.Payload) == 0 {
return nil, false
}
parts := envelope.Payload
var marker string
if json.Unmarshal(parts[0], &marker) != nil || marker != "frida:rpc" {
return nil, false
}
return decodeScriptRPCPayload(parts[1:]), true
}

func decodeScriptRPCPayload(parts []json.RawMessage) *scriptRPCReply {
var reply scriptRPCReply
if len(parts) < 3 || json.Unmarshal(parts[0], &reply.ID) != nil || reply.ID == "" {
return nil
}
var status string
if json.Unmarshal(parts[1], &status) != nil || (status != "ok" && status != "error") {
return nil
}
// ExportsCall returns the value for both success and error replies.
if json.Unmarshal(parts[2], &reply.Value) != nil {
return nil
}
return &reply
}
Loading
0