4 Commits
10 changed files with 659 additions and 119 deletions

No files matched your search

-4
View File
@@ -12,7 +12,6 @@ Exit codes: 0 on success (changed or not), 1 when a version check fails.
"""
import json
import os
import re
import sys
import urllib.request
@@ -46,9 +45,6 @@ def latest_release(repo):
"User-Agent": "rz-frida-version-bump",
},
)
token = os.environ.get("GITHUB_TOKEN")
if token:
request.add_header("Authorization", "Bearer %s" % token)
with urllib.request.urlopen(request, timeout=30) as response:
data = json.load(response)
return data["tag_name"]
+303
View File
@@ -0,0 +1,303 @@
# SPDX-FileCopyrightText: 2026 Alok Kumar Mishra <[email protected]>
# SPDX-License-Identifier: LGPL-3.0-only
"""Validate a dependency pin patch before it is uploaded or published.
The only accepted changes are version rewrites of the pin lines in the
workflow env blocks, of the meson frida_version option, and of the Cutter
version examples in the README. File creates, deletes, renames, copies,
mode or binary changes, non-pin edits, and malformed diffs are rejected.
"""
import re
import sys
ALLOWED_PATHS = (
".github/workflows/ci.yml",
".github/workflows/release.yml",
"meson_options.txt",
"README.md",
)
# Pin names that exist in each workflow env block.
WORKFLOW_PINS = {
".github/workflows/ci.yml": frozenset(("RIZIN_VERSION", "FRIDA_VERSION")),
".github/workflows/release.yml": frozenset(
("RIZIN_VERSION", "FRIDA_VERSION", "CUTTER_VERSION")
),
}
VERSION = r"v?\d+(?:\.\d+)+"
DIFF_HEADER = re.compile(r"^diff --git a/(\S+) b/(\S+)$")
INDEX_LINE = re.compile(r"^index [0-9a-f]{4,64}\.\.[0-9a-f]{4,64} 100644$")
HUNK_HEADER = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$")
PIN_LINE = re.compile(
r"^ (RIZIN_VERSION|FRIDA_VERSION|CUTTER_VERSION): (" + VERSION + r")$"
)
MESON_LINE = re.compile(
r"^option\('frida_version', type: 'string', value: '("
+ VERSION
+ r")', description: ('[^']*')\)$"
)
# The Cutter version mentions the bump script rewrites in the README.
README_FILENAME = re.compile(r"Cutter-(" + VERSION + r")-(Linux|macOS|Windows)")
README_PHRASE = re.compile(r"Cutter (" + VERSION + r") AppImage")
class PatchError(Exception):
"""The patch is not a plain dependency version bump."""
def validate_text(text):
"""Raise PatchError unless text is a clean version-only pin patch."""
if not text.strip():
raise PatchError("patch is empty")
lines = text.splitlines()
paths = set()
index = 0
while index < len(lines):
index = _parse_file(lines, index, paths)
if not paths:
raise PatchError("patch has no file diffs")
def _parse_file(lines, index, paths):
header = DIFF_HEADER.fullmatch(lines[index])
if header is None:
raise PatchError(
"line %d: expected a 'diff --git' header, got %r"
% (index + 1, lines[index])
)
old_path, new_path = header.group(1), header.group(2)
if old_path != new_path:
raise PatchError(
"line %d: diff header renames %s to %s" % (index + 1, old_path, new_path)
)
if old_path not in ALLOWED_PATHS:
raise PatchError(
"line %d: %s is not an update target" % (index + 1, old_path)
)
if old_path in paths:
raise PatchError("line %d: duplicate diff for %s" % (index + 1, old_path))
paths.add(old_path)
index += 1
if index >= len(lines) or INDEX_LINE.fullmatch(lines[index]) is None:
raise PatchError(
"line %d: %s: expected an 'index <hash>..<hash> 100644' line"
% (index + 1, old_path)
)
index += 1
for marker in ("--- a/" + old_path, "+++ b/" + old_path):
if index >= len(lines) or lines[index] != marker:
raise PatchError(
"line %d: %s: expected %r"
% (index + 1, old_path, marker)
)
index += 1
blocks = []
hunks = 0
while index < len(lines) and not lines[index].startswith("diff --git "):
hunk = HUNK_HEADER.fullmatch(lines[index])
if hunk is None:
raise PatchError(
"line %d: %s: malformed hunk header %r"
% (index + 1, old_path, lines[index])
)
index += 1
index, hunk_blocks = _parse_hunk(lines, index, old_path, hunk)
blocks.extend(hunk_blocks)
hunks += 1
if hunks == 0:
raise PatchError("%s: file diff has no hunks" % old_path)
_validate_blocks(old_path, blocks)
return index
def _parse_hunk(lines, index, path, header):
old_left = int(header.group(2)) if header.group(2) is not None else 1
new_left = int(header.group(4)) if header.group(4) is not None else 1
blocks = []
minus = []
plus = []
seen_plus = False
def flush():
nonlocal minus, plus, seen_plus
if minus or plus:
blocks.append((minus, plus))
minus, plus, seen_plus = [], [], False
while old_left > 0 or new_left > 0:
if index >= len(lines):
raise PatchError(
"line %d: %s: hunk is truncated, missing %d old and %d new lines"
% (index + 1, path, old_left, new_left)
)
line = lines[index]
if line.startswith(" "):
flush()
old_left -= 1
new_left -= 1
elif line.startswith("-"):
if seen_plus:
raise PatchError(
"line %d: %s: '-' after '+' in a change block"
% (index + 1, path)
)
minus.append(line[1:])
old_left -= 1
elif line.startswith("+"):
plus.append(line[1:])
seen_plus = True
new_left -= 1
else:
raise PatchError(
"line %d: %s: malformed diff line %r" % (index + 1, path, line)
)
if old_left < 0 or new_left < 0:
raise PatchError(
"line %d: %s: hunk has more lines than its header declares"
% (index + 1, path)
)
index += 1
flush()
if not blocks:
raise PatchError("%s: hunk changes no lines" % path)
return index, blocks
def _validate_blocks(path, blocks):
if path in WORKFLOW_PINS:
_validate_workflow_blocks(path, blocks)
elif path == "meson_options.txt":
_validate_meson_blocks(blocks)
else:
_validate_readme_blocks(blocks)
def _validate_workflow_blocks(path, blocks):
pins = WORKFLOW_PINS[path]
for minus, plus in blocks:
if len(minus) != len(plus):
raise PatchError("%s: pin lines must be edited in place" % path)
for old_line, new_line in zip(minus, plus):
old = PIN_LINE.fullmatch(old_line)
new = PIN_LINE.fullmatch(new_line)
if old is None or new is None:
raise PatchError(
"%s: not a pin line: %r -> %r" % (path, old_line, new_line)
)
name = old.group(1)
if new.group(1) != name:
raise PatchError(
"%s: pin %s was turned into %s" % (path, name, new.group(1))
)
if name not in pins:
raise PatchError("%s: %s is not pinned in this workflow" % (path, name))
if old.group(2) == new.group(2):
raise PatchError(
"%s: pin %s keeps the version %s" % (path, name, old.group(2))
)
def _validate_meson_blocks(blocks):
for minus, plus in blocks:
if len(minus) != len(plus):
raise PatchError("meson_options.txt: option line must be edited in place")
pairs = [pair for minus, plus in blocks for pair in zip(minus, plus)]
if len(pairs) != 1:
raise PatchError(
"meson_options.txt: expected exactly one frida_version line edit"
)
old_line, new_line = pairs[0]
old = MESON_LINE.fullmatch(old_line)
new = MESON_LINE.fullmatch(new_line)
if old is None or new is None:
raise PatchError(
"meson_options.txt: not the frida_version option: %r -> %r"
% (old_line, new_line)
)
if old.group(2) != new.group(2):
raise PatchError(
"meson_options.txt: frida_version edit changes more than the version"
)
if old.group(1) == new.group(1):
raise PatchError(
"meson_options.txt: frida_version keeps the value %s" % old.group(1)
)
def _readme_versions(line):
versions = [match.group(1) for match in README_FILENAME.finditer(line)]
versions.extend(match.group(1) for match in README_PHRASE.finditer(line))
return versions
def _replace_readme_versions(line, version):
line = README_FILENAME.sub(
lambda match: "Cutter-%s-%s" % (version, match.group(2)), line
)
return README_PHRASE.sub("Cutter %s AppImage" % version, line)
def _validate_readme_blocks(blocks):
old_versions = set()
new_versions = set()
for minus, plus in blocks:
if len(minus) != len(plus):
raise PatchError("README.md: version mentions must be edited in place")
for old_line, new_line in zip(minus, plus):
old_found = _readme_versions(old_line)
new_found = _readme_versions(new_line)
if not old_found or not new_found:
raise PatchError(
"README.md: not a Cutter version mention: %r" % old_line
)
if len(set(old_found)) != 1 or len(set(new_found)) != 1:
raise PatchError(
"README.md: mixed Cutter versions in %r -> %r"
% (old_line, new_line)
)
old_version = old_found[0]
new_version = new_found[0]
if old_version == new_version:
raise PatchError("README.md: %s keeps the version" % old_version)
expected = _replace_readme_versions(old_line, new_version)
if expected != new_line:
raise PatchError(
"README.md: %r is not the expected version substitution of %r"
% (new_line, old_line)
)
old_versions.add(old_version)
new_versions.add(new_version)
if len(old_versions) != 1 or len(new_versions) != 1:
raise PatchError("README.md: inconsistent Cutter versions in the diff")
def main(argv):
if len(argv) != 2:
print("usage: validate_pins_patch.py <patch>", file=sys.stderr)
return 2
try:
with open(argv[1], encoding="utf-8") as handle:
text = handle.read()
except OSError as error:
print("error: cannot read %s: %s" % (argv[1], error), file=sys.stderr)
return 2
try:
validate_text(text)
except PatchError as error:
print("::error::pins.patch rejected: %s" % error, file=sys.stderr)
return 1
print("pins.patch ok")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+43 -16
View File
@@ -2,14 +2,19 @@ name: CI
on:
push:
branches: [main]
branches: [dev, stable]
pull_request:
branches: [main]
workflow_dispatch:
# Dependency pins, bumped by update-deps workflow.
# dev builds rizin from its dev branch, stable uses the pinned release.
# RIZIN_DEV_REF can be a branch name or a commit sha (just-in-case).
# PRs use the base branch, pushes/dispatches use the ref.
env:
RIZIN_VERSION: v0.9.1
RIZIN_DEV_REF: dev
FRIDA_VERSION: 17.17.0
RIZIN_FLAVOR: ${{ (github.base_ref == 'dev' || github.ref_name == 'dev') && 'dev' || 'release' }}
jobs:
linux-frida:
@@ -22,15 +27,26 @@ jobs:
with: { node-version: '24' }
- name: Install rizin
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
pip install meson ninja PyYAML
wget -q "https://github.com/rizinorg/rizin/releases/download/${RIZIN_VERSION}/rizin-src-${RIZIN_VERSION}.tar.xz"
tar xf rizin-src-${RIZIN_VERSION}.tar.xz
cd rizin-${RIZIN_VERSION}
meson setup build --prefix=/usr -Dblob=true
ninja -C build
sudo ninja -C build install
cd ..
if [ "$RIZIN_FLAVOR" = dev ]; then
curl -fsSL "https://github.com/rizinorg/rizin/archive/${RIZIN_DEV_REF}.tar.gz" -o rizin-dev.tar.gz
mkdir -p rizin-dev-src
tar xf rizin-dev.tar.gz -C rizin-dev-src --strip-components=1
src=rizin-dev-src
echo "rizin dev ref: ${RIZIN_DEV_REF} ($(gh api "repos/rizinorg/rizin/commits/${RIZIN_DEV_REF}" --jq .sha 2>/dev/null || echo unknown))"
else
wget -q "https://github.com/rizinorg/rizin/releases/download/${RIZIN_VERSION}/rizin-src-${RIZIN_VERSION}.tar.xz"
tar xf "rizin-src-${RIZIN_VERSION}.tar.xz"
src="rizin-${RIZIN_VERSION}"
echo "rizin release: ${RIZIN_VERSION}"
fi
meson setup "$src/build" "$src" --prefix=/usr -Dblob=true
ninja -C "$src/build"
sudo ninja -C "$src/build" install
- name: Build (with frida-core)
run: |
@@ -56,15 +72,26 @@ jobs:
with: { node-version: '24' }
- name: Install rizin
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
pip install meson ninja PyYAML
wget -q "https://github.com/rizinorg/rizin/releases/download/${RIZIN_VERSION}/rizin-src-${RIZIN_VERSION}.tar.xz"
tar xf rizin-src-${RIZIN_VERSION}.tar.xz
cd rizin-${RIZIN_VERSION}
meson setup build --prefix=/usr -Dblob=true
ninja -C build
sudo ninja -C build install
cd ..
if [ "$RIZIN_FLAVOR" = dev ]; then
curl -fsSL "https://github.com/rizinorg/rizin/archive/${RIZIN_DEV_REF}.tar.gz" -o rizin-dev.tar.gz
mkdir -p rizin-dev-src
tar xf rizin-dev.tar.gz -C rizin-dev-src --strip-components=1
src=rizin-dev-src
echo "rizin dev ref: ${RIZIN_DEV_REF} ($(gh api "repos/rizinorg/rizin/commits/${RIZIN_DEV_REF}" --jq .sha 2>/dev/null || echo unknown))"
else
wget -q "https://github.com/rizinorg/rizin/releases/download/${RIZIN_VERSION}/rizin-src-${RIZIN_VERSION}.tar.xz"
tar xf "rizin-src-${RIZIN_VERSION}.tar.xz"
src="rizin-${RIZIN_VERSION}"
echo "rizin release: ${RIZIN_VERSION}"
fi
meson setup "$src/build" "$src" --prefix=/usr -Dblob=true
ninja -C "$src/build"
sudo ninja -C "$src/build" install
- name: Build (without frida-core)
run: |
+1 -1
View File
@@ -3,7 +3,7 @@ name: Lint
on:
pull_request:
push:
branches: [main]
branches: [dev, stable]
jobs:
changes:
+19 -1
View File
@@ -2,6 +2,7 @@ name: Release
# Builds and tests the rizin and cutter plugins
# and optionally publishes a GitHub release.
# Releases are from the stable branch, dev has no releases.
on:
workflow_dispatch:
@@ -27,9 +28,11 @@ on:
pull_request:
branches:
- '**release**'
- stable
push:
branches:
- '**release**'
- stable
# Dependency pins, bumped by the update-deps workflow.
env:
@@ -49,6 +52,14 @@ jobs:
name: Test gate
runs-on: ubuntu-24.04
steps:
- name: Require stable branch for publishing
if: github.event_name == 'workflow_dispatch' && inputs.publish_release
run: |
if [ "$GITHUB_REF_NAME" != stable ]; then
echo "::error::releases must be dispatched from the stable branch"
exit 1
fi
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: { python-version: '3.12' }
@@ -939,6 +950,13 @@ jobs:
- package-checksums
if: github.event_name == 'workflow_dispatch' && inputs.publish_release
steps:
- name: Require stable branch
run: |
if [ "$GITHUB_REF_NAME" != stable ]; then
echo "::error::releases must be dispatched from the stable branch"
exit 1
fi
- uses: actions/download-artifact@v8
with:
name: rz-frida-release-assets
@@ -985,6 +1003,6 @@ jobs:
target_commitish: ${{ steps.meta.outputs.target_commitish }}
generate_release_notes: true
draft: false
prerelease: ${{ inputs.prerelease == 'true' }}
prerelease: ${{ inputs.prerelease }}
fail_on_unmatched_files: true
files: release-assets/*
+171
View File
@@ -0,0 +1,171 @@
name: Open dependency pin pull request
# Publisher for the diffs produced by the read-only "Update dependency pins"
# collector. It only downloads that run's validated patches, applies them, and
# pushes branches / opens the PRs. All patch validation stuff happens in the
# collector.
on:
workflow_run:
workflows: ["Update dependency pins"]
types: [completed]
permissions:
actions: read
contents: read
jobs:
open:
name: Open pull request (${{ matrix.target }})
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request' &&
(github.event.workflow_run.event == 'schedule' ||
github.event.workflow_run.event == 'workflow_dispatch') &&
github.event.workflow_run.head_branch == 'dev' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
concurrency:
group: update-deps-pr-${{ matrix.target }}
cancel-in-progress: false
strategy:
fail-fast: false
matrix:
target: [dev, stable]
env:
BASE_BRANCH: ${{ matrix.target }}
ARTIFACT: pins-patch-${{ matrix.target }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
ref: ${{ matrix.target }}
- name: Download pin diff
id: download
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
set -euo pipefail
case "$BASE_BRANCH" in
dev|stable) ;;
*)
echo "::error::unexpected target branch: $BASE_BRANCH"
exit 1
;;
esac
count=$(gh api --paginate \
"repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/artifacts" \
--jq ".artifacts[] | select(.name == \"${ARTIFACT}\" and .expired == false) | .id" \
| wc -l)
case "$count" in
0)
echo "no pin diff to publish for ${BASE_BRANCH}"
exit 0
;;
1) ;;
*)
echo "::error::expected one non-expired ${ARTIFACT} artifact, found ${count}"
exit 1
;;
esac
dest="${RUNNER_TEMP}/pins-patch"
rm -rf "$dest"
gh run download "$RUN_ID" --repo "$GITHUB_REPOSITORY" \
--name "$ARTIFACT" --dir "$dest"
mapfile -d '' patches < <(find "$dest" -type f -name pins.patch -print0)
if [ "${#patches[@]}" -ne 1 ]; then
echo "::error::expected exactly one pins.patch in the artifact, found ${#patches[@]}"
exit 1
fi
# Fixed copy path, so no artifact-controlled name reaches later steps.
cp -- "${patches[0]}" "${RUNNER_TEMP}/pins.patch"
echo "found=true" >> "$GITHUB_OUTPUT"
- name: Apply pin diff
if: steps.download.outputs.found == 'true'
run: |
set -euo pipefail
patch="${RUNNER_TEMP}/pins.patch"
git apply --check "$patch"
git apply "$patch"
git add -- .github/workflows/ci.yml .github/workflows/release.yml meson_options.txt README.md
status=$(git status --porcelain --untracked-files=all)
if [ -z "$status" ]; then
echo "::error::pin diff produced no staged changes"
exit 1
fi
unexpected=$(printf '%s\n' "$status" | grep -Ev '^M (\.github/workflows/ci\.yml|\.github/workflows/release\.yml|meson_options\.txt|README\.md)$' || true)
if [ -n "$unexpected" ]; then
echo "::error::pin diff touched paths outside the four pin files"
printf '%s\n' "$unexpected" >&2
exit 1
fi
summary=$(git diff --cached --summary)
if [ -n "$summary" ]; then
echo "::error::pin diff changes a file type or mode"
printf '%s\n' "$summary" >&2
exit 1
fi
git -c user.name='github-actions[bot]' \
-c user.email='41898282+github-actions[bot]@users.noreply.github.com' \
commit -m "update ${BASE_BRANCH} dependency pins"
- name: Push branch and open pull request
if: steps.download.outputs.found == 'true'
env:
GH_TOKEN: ${{ secrets.DEPENDENCY_PIN_PR_TOKEN }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::DEPENDENCY_PIN_PR_TOKEN is not set; cannot publish the pin pr"
exit 1
fi
case "$BASE_BRANCH" in
stable)
branch=deps/update-release-pins
title='update release dependency pins'
body=$(printf '%s\n' \
'## Summary' \
'- bump the rizin / frida / Cutter version pins in the workflows to their latest releases' \
'' \
'Release prep input: merge once stable code is compatible with the bump.')
;;
dev)
branch=deps/update-dev-pins
title='update dev dependency pins'
body=$(printf '%s\n' \
'## Summary' \
'- bump the dependency pins in the workflows to their latest releases' \
'' \
'dev CI builds rizin from its dev branch, so rizin pin bumps affect release prep only.')
;;
*)
echo "::error::unexpected target branch: $BASE_BRANCH"
exit 1
;;
esac
gh auth setup-git
if git ls-remote --exit-code origin "refs/heads/${branch}" >/dev/null 2>&1; then
git fetch origin "refs/heads/${branch}:refs/remotes/origin/${branch}"
git push --force-with-lease="refs/heads/${branch}:refs/remotes/origin/${branch}" \
origin "HEAD:refs/heads/${branch}"
else
git push origin "HEAD:refs/heads/${branch}"
fi
state=$(gh pr view "$branch" --repo "$GITHUB_REPOSITORY" --json state --jq .state 2>/dev/null || true)
case "$state" in
OPEN)
gh pr edit "$branch" --repo "$GITHUB_REPOSITORY" \
--base "$BASE_BRANCH" --title "$title" --body "$body"
;;
CLOSED)
gh pr reopen "$branch" --repo "$GITHUB_REPOSITORY"
gh pr edit "$branch" --repo "$GITHUB_REPOSITORY" \
--base "$BASE_BRANCH" --title "$title" --body "$body"
;;
*)
gh pr create --repo "$GITHUB_REPOSITORY" --base "$BASE_BRANCH" --head "$branch" --title "$title" --body "$body"
;;
esac
+37 -23
View File
@@ -1,7 +1,8 @@
name: Update dependency pins
# Checks latest rizin / frida / Cutter releases once a week and opens a
# pr when a pinned version in the release workflow is outdated.
# Read-only collector: reads the latest rizin, frida, and Cutter releases,
# rewrites the version pins when they are outdated, validates the diff, and
# uploads it. "Open dependency pin pull request" publishes it.
on:
schedule:
@@ -9,44 +10,57 @@ on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
contents: read
concurrency:
group: update-deps
group: update-deps-collect
cancel-in-progress: true
jobs:
bump:
name: Check for newer dependency releases
collect:
name: Check for newer dependency releases (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [dev, stable]
steps:
- uses: actions/checkout@v7
with:
ref: ${{ matrix.target }}
persist-credentials: false
- name: Bump version pins
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ""
GH_TOKEN: ""
run: python3 .github/scripts/bump_release_versions.py
- name: Check for changes
- name: Save pin diff
id: diff
run: |
set -euo pipefail
if git diff --quiet; then
echo "changed=false" >> $GITHUB_OUTPUT
else
git diff
echo "changed=true" >> $GITHUB_OUTPUT
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
unexpected=$(git diff --name-only | grep -Ev '^(\.github/workflows/ci\.yml|\.github/workflows/release\.yml|meson_options\.txt|README\.md)$' || true)
if [ -n "$unexpected" ]; then
echo "bump edited a file that is not a pin file" >&2
printf '%s\n' "$unexpected" >&2
exit 1
fi
git diff --no-ext-diff -- .github/workflows/ci.yml .github/workflows/release.yml meson_options.txt README.md > pins.patch
echo "changed=true" >> "$GITHUB_OUTPUT"
- name: Create pull request
- name: Validate pin diff
if: steps.diff.outputs.changed == 'true'
run: python3 .github/scripts/validate_pins_patch.py pins.patch
- uses: actions/upload-artifact@v7
if: steps.diff.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: deps/update-release-pins
title: 'update release dependency pins'
body: |
## Summary
- bump the rizin / frida / Cutter version pins in the workflows to their latest releases
commit-message: 'update release dependency pins'
delete-branch: true
name: pins-patch-${{ matrix.target }}
path: pins.patch
retention-days: 5
if-no-files-found: error
+10
View File
@@ -4,6 +4,16 @@ Frida integration plugin for Rizin and Cutter.
The Rizin plugin provides the backend and command interface. The Cutter plugin provides a native frontend over the Rizin backend.
## Branches
- `dev` (default): development branch. CI builds against rizin `dev`.
- `stable`: release branch. Uses the rizin, frida, and Cutter releases
pinned in the workflows. Fixes that apply to the released APIs are
cherry-picked here, and releases are tagged here.
Pull requests target `dev` by default. Dependency pin PRs are opened
against both branches when a newer release is available.
## Screenshots
![Rizin CLI command help](.images/rizin-cli.png)
+7 -6
View File
@@ -2,12 +2,13 @@
The plugin is a rizin core plugin: it registers the `Fr` command group
inside rizin. It needs rizin v0.9.0 or later (the per-core plugin context
from [rizin#6404](https://github.com/rizinorg/rizin/pull/6404)). CI builds
against v0.9.1. It also needs rizin's development files. With
`-Dfrida_core=enabled` and no manual paths, meson fetches the frida-core
devkit for the host (see `frida_version` in `meson_options.txt`). Without
frida-core the plugin still builds, and device commands answer with a
`frida_unavailable` error.
from [rizin#6404](https://github.com/rizinorg/rizin/pull/6404)). The
`stable` branch builds against the rizin release pinned in the workflows
and the `dev` branch builds against rizin `dev`. It also needs rizin's
development files. With `-Dfrida_core=enabled` and no manual paths,
meson fetches the frida-core devkit for the host (see `frida_version`
in `meson_options.txt`). Without frida-core the plugin still builds,
and device commands answer with a `frida_unavailable` error.
The examples below use a prefix of `$PREFIX`. Replace it with wherever you
install rizin (`/usr/local`, `$HOME/.local`, and so on). `rizin -H` prints
+68 -68
View File
@@ -49,12 +49,12 @@ static RzCmdStatus print_status(RzCore *core, RzCmdStateOutput *state) {
switch (state->mode) {
case RZ_OUTPUT_MODE_STANDARD:
rz_cons_printf("active: %s\n", rz_str_bool(active));
rz_cons_printf("state: %s\n", state_string);
rz_cons_printf(core->cons,"active: %s\n", rz_str_bool(active));
rz_cons_printf(core->cons,"state: %s\n", state_string);
if (session) {
rz_cons_printf("pid: %u\n", rz_frida_session_target_pid(session));
rz_cons_printf("action: %s\n", uri->action);
rz_cons_printf("target: %s\n", uri->target);
rz_cons_printf(core->cons,"pid: %u\n", rz_frida_session_target_pid(session));
rz_cons_printf(core->cons,"action: %s\n", uri->action);
rz_cons_printf(core->cons,"target: %s\n", uri->target);
}
return RZ_CMD_STATUS_OK;
case RZ_OUTPUT_MODE_JSON:
@@ -74,7 +74,7 @@ static RzCmdStatus print_status(RzCore *core, RzCmdStateOutput *state) {
}
}
static RzCmdStatus print_uri(const char *uri_string, RzCmdStateOutput *state) {
static RzCmdStatus print_uri(const char *uri_string, RzCmdStateOutput *state, RzCons* cons) {
rz_return_val_if_fail(state, RZ_CMD_STATUS_ERROR);
RzFridaUri uri = { 0 };
@@ -102,10 +102,10 @@ static RzCmdStatus print_uri(const char *uri_string, RzCmdStateOutput *state) {
switch (state->mode) {
case RZ_OUTPUT_MODE_STANDARD:
rz_cons_printf("action: %s\n", uri.action);
rz_cons_printf("transport: %s\n", uri.transport);
rz_cons_printf("device: %s\n", uri.device);
rz_cons_printf("target: %s\n", uri.target);
rz_cons_printf(cons,"action: %s\n", uri.action);
rz_cons_printf(cons,"transport: %s\n", uri.transport);
rz_cons_printf(cons,"device: %s\n", uri.device);
rz_cons_printf(cons,"target: %s\n", uri.target);
break;
case RZ_OUTPUT_MODE_JSON:
rz_frida_json_ok_begin(state->d.pj);
@@ -132,7 +132,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridas_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
RZ_IPI RzCmdStatus rz_cmd_fridau_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int argc, RZ_NONNULL const char **argv, RZ_NONNULL RzCmdStateOutput *state) {
rz_return_val_if_fail(core && argv && state, RZ_CMD_STATUS_ERROR);
return print_uri(argv[1], state);
return print_uri(argv[1], state, core->cons);
}
RZ_IPI RzCmdStatus rz_cmd_fridad_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int argc, RZ_NONNULL const char **argv, RZ_NONNULL RzCmdStateOutput *state) {
@@ -246,9 +246,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridao_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
// backend_open writes the envelope either way. only keep the session if it opened.
// the cons break hook forwards a Cutter breakTask (or Ctrl-C) into the session's
// cancellable, which frida's blocking attach/connect uses.
rz_cons_break_push(frida_cancel_on_break, session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, session);
bool opened = rz_frida_backend_open(session, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
if (!opened) {
rz_frida_session_free(session);
return RZ_CMD_STATUS_OK;
@@ -314,9 +314,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridae_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_eval(ctx->session, argv[1], pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -338,9 +338,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridal_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET, "cannot read the script file");
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_eval(ctx->session, source, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
free(source);
return RZ_CMD_STATUS_OK;
}
@@ -358,9 +358,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridai_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_ping(ctx->session, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -401,9 +401,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridax_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
}
ut64 address = rz_num_math(core->num, argv[1]);
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_mem_read(ctx->session, address, size, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -478,9 +478,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaw_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
return RZ_CMD_STATUS_OK;
}
ut64 address = rz_num_math(core->num, argv[1]);
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_mem_write(ctx->session, address, bytes, (size_t)len, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
free(bytes);
return RZ_CMD_STATUS_OK;
}
@@ -500,9 +500,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaR_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
// any arg forces a fresh enumeration instead of cached ranges.
bool refresh = (argc > 1) && RZ_STR_ISNOTEMPTY(argv[1]);
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_ranges(ctx->session, refresh, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -519,9 +519,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridat_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_threads(ctx->session, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -540,9 +540,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaM_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
// any arg forces fresh enum instead of cached modules.
bool refresh = (argc > 1) && RZ_STR_ISNOTEMPTY(argv[1]);
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_modules(ctx->session, refresh, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -559,9 +559,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaE_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_exports(ctx->session, argv[1], pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -578,9 +578,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaI_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_imports(ctx->session, argv[1], pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -597,9 +597,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaS_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_symbols(ctx->session, argv[1], pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -617,14 +617,14 @@ RZ_IPI RzCmdStatus rz_cmd_fridab_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
}
// an addr sets a breakpoint, no arg lists the current breakpoints.
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
if (argc > 1 && RZ_STR_ISNOTEMPTY(argv[1])) {
ut64 address = rz_num_math(core->num, argv[1]);
rz_frida_backend_bp_set(ctx->session, address, pj);
} else {
rz_frida_backend_bp_list(ctx->session, pj);
}
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -648,9 +648,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridab_minus_handler(RZ_NONNULL RzCore *core, RZ_UNUSE
rz_strf(address_str, "0x%" PFMT64x, address);
target = address_str;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_bp_remove(ctx->session, target, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -674,9 +674,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridag_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
rz_strf(tid, "%" PFMT64u, rz_num_math(core->num, argv[1]));
thread_id = tid;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_continue(ctx->session, thread_id, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -694,7 +694,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaB_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
}
ut64 thread_id = rz_num_math(core->num, argv[1]);
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
if (argc > 3) {
char value_str[32];
rz_strf(value_str, "0x%" PFMT64x, rz_num_math(core->num, argv[3]));
@@ -704,7 +704,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaB_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
} else {
rz_frida_backend_reg_read(ctx->session, thread_id, pj);
}
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -722,7 +722,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaW_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
}
// addr sets watchpoint, no arg lists current wps.
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
if (argc > 1 && RZ_STR_ISNOTEMPTY(argv[1])) {
ut64 address = rz_num_math(core->num, argv[1]);
ut64 size = (argc > 2) ? rz_num_math(core->num, argv[2]) : 0;
@@ -732,7 +732,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaW_handler(RZ_NONNULL RzCore *core, int argc, RZ_N
} else {
rz_frida_backend_wp_list(ctx->session, pj);
}
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -756,9 +756,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaW_minus_handler(RZ_NONNULL RzCore *core, RZ_UNUSE
rz_strf(address_str, "0x%" PFMT64x, address);
target = address_str;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_wp_remove(ctx->session, target, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -773,9 +773,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaJ_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET, "no session is open");
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_java_available(ctx->session, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -790,9 +790,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaL_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET, "no session is open");
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_loaders(ctx->session, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -810,9 +810,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaC_handler(RZ_NONNULL RzCore *core, int argc,
}
const char *prefix = (argc > 1 && RZ_STR_ISNOTEMPTY(argv[1])) ? argv[1] : NULL;
ut64 max = rz_config_get_integer(core->config, "frida.java.max");
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_classes(ctx->session, prefix, max, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -829,7 +829,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaN_handler(RZ_NONNULL RzCore *core, int argc,
return RZ_CMD_STATUS_OK;
}
bool enable = true;
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
if (argc > 1 && RZ_STR_ISNOTEMPTY(argv[1])) {
if (RZ_STR_EQ(argv[1], "start")) {
enable = true;
@@ -838,14 +838,14 @@ RZ_IPI RzCmdStatus rz_cmd_fridaN_handler(RZ_NONNULL RzCore *core, int argc,
} else {
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET,
"FrNj expects start, stop, or no arguments to list");
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
rz_frida_backend_class_load_monitor(ctx->session, enable, pj);
} else {
rz_frida_backend_newly_loaded_classes(ctx->session, pj);
}
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -861,7 +861,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaRN_handler(RZ_NONNULL RzCore *core, int argc,
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET, "no session is open");
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
if (argc > 1 && RZ_STR_ISNOTEMPTY(argv[1])) {
if (RZ_STR_EQ(argv[1], "on")) {
rz_frida_backend_rn_set(ctx->session, true, pj);
@@ -872,13 +872,13 @@ RZ_IPI RzCmdStatus rz_cmd_fridaRN_handler(RZ_NONNULL RzCore *core, int argc,
} else {
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET,
"FrRNj expects on, off, import, or no arguments to list");
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
} else {
rz_frida_backend_rn_list(ctx->session, pj);
}
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -894,9 +894,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaf_handler(RZ_NONNULL RzCore *core, RZ_UNUSED int
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET, "no session is open");
return RZ_CMD_STATUS_OK;
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_flag_import(ctx->session, core, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -913,9 +913,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaX_handler(RZ_NONNULL RzCore *core, int argc,
return RZ_CMD_STATUS_OK;
}
const char *prefix = (argc > 1 && RZ_STR_ISNOTEMPTY(argv[1])) ? argv[1] : NULL;
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_dex_diff(ctx->session, core, prefix, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -940,9 +940,9 @@ RZ_IPI RzCmdStatus rz_cmd_fridaD_handler(RZ_NONNULL RzCore *core, int argc,
if (argc > 2 && RZ_STR_ISNOTEMPTY(argv[2])) {
loaderId = rz_num_math(core->num, argv[2]);
}
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
rz_frida_backend_class_describe(ctx->session, className, loaderId, pj);
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -969,7 +969,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaIm_handler(RZ_NONNULL RzCore *core, int argc,
}
bool has_dot = (strchr(className, '.') != NULL);
rz_cons_break_push(frida_cancel_on_break, ctx->session);
rz_interrupt_break_push(core->intr, frida_cancel_on_break, ctx->session);
if (has_dot) {
rz_frida_backend_import_class(ctx->session, core, className, loaderId, pj);
} else {
@@ -983,7 +983,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaIm_handler(RZ_NONNULL RzCore *core, int argc,
}
rz_frida_json_error(pj, RZ_FRIDA_ERROR_INVALID_TARGET,
"no matching classes found for the given prefix");
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}
@@ -1026,7 +1026,7 @@ RZ_IPI RzCmdStatus rz_cmd_fridaIm_handler(RZ_NONNULL RzCore *core, int argc,
pj_kn(pj, "constructors", (ut64)total_c);
rz_frida_json_ok_end(pj);
}
rz_cons_break_pop();
rz_interrupt_break_pop(core->intr);
return RZ_CMD_STATUS_OK;
}