commit 6febb83834089f9597c338836c2b02aefbcb69d5 Author: LoveSy Date: Wed Feb 16 07:24:35 2022 +0800 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..edda2b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.cmd text eol=crlf +*.bat text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.so binary +*.dex binary +*.jar binary +*.png binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..79b0d1e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "library/jni/external/dex_builder"] + path = library/jni/external/dex_builder + url = https://github.com/LSPosed/DexBuilder.git + branch = master diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1c94431 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2022 LSPosed + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c92b16 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# LSPlant diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..32aa357 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,42 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2021 LSPosed Contributors + */ +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.internal.storage.file.FileRepository + +buildscript { + repositories { + google() + mavenCentral() + } + val agpVersion by extra("7.1.1") + dependencies { + classpath("com.android.tools.build:gradle:$agpVersion") + classpath("org.eclipse.jgit:org.eclipse.jgit:6.0.0.202111291000-r") + } +} + +val androidTargetSdkVersion by extra(32) +val androidMinSdkVersion by extra(27) +val androidBuildToolsVersion by extra("32.0.0") +val androidCompileSdkVersion by extra(32) +val androidCompileNdkVersion by extra("23.1.7779620") + +tasks.register("Delete", Delete::class) { + delete(rootProject.buildDir) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f798c6a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2e6e589 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..48c0167 --- /dev/null +++ b/library/.gitignore @@ -0,0 +1,5 @@ +/build +/libs +/obj +/release + diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 0000000..2a6ce83 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,65 @@ +/* + * This file is part of LSPosed. + * + * LSPosed is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LSPosed is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LSPosed. If not, see . + * + * Copyright (C) 2021 LSPosed Contributors + */ + +plugins { + id("com.android.library") +} + +val androidTargetSdkVersion: Int by rootProject.extra +val androidMinSdkVersion: Int by rootProject.extra +val androidBuildToolsVersion: String by rootProject.extra +val androidCompileSdkVersion: Int by rootProject.extra +val androidCompileNdkVersion: String by rootProject.extra + +android { + compileSdk = androidCompileSdkVersion + ndkVersion = androidCompileNdkVersion + buildToolsVersion = androidBuildToolsVersion + + buildFeatures { + prefab = true + } + + defaultConfig { + minSdk = androidMinSdkVersion + targetSdk = androidTargetSdkVersion + + externalNativeBuild { + ndkBuild { + arguments += "-j${Runtime.getRuntime().availableProcessors()}" + } + } + } + + lint { + abortOnError = true + checkReleaseBuilds = false + } + + externalNativeBuild { + ndkBuild { + path("jni/Android.mk") + } + } +} + + +dependencies { + implementation("dev.rikka.ndk.thirdparty:cxx:1.2.0") +} diff --git a/library/jni/.clang-tidy b/library/jni/.clang-tidy new file mode 100644 index 0000000..3b78393 --- /dev/null +++ b/library/jni/.clang-tidy @@ -0,0 +1,63 @@ +--- +Checks: > + -*, + bugprone-*, + google-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + clang-analyzer-*, + llvm-include-order, + -modernize-use-trailing-return-type, + -readability-implicit-bool-conversion, + -performance-no-int-to-ptr, + +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.ClassMemberCase + value: lower_case + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantPrefix + value: k + - key: readability-identifier-naming.FunctionCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantPrefix + value: k + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantPrefix + value: k + - key: readability-identifier-naming.StaticVariableCase + value: CamelCase + - key: readability-identifier-naming.StaticVariablePrefix + value: k + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberSuffix + value: _ + - key: readability-identifier-naming.ProtectedMemberSuffix + value: _ + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.TypedefCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.IgnoreMainLikeFunctions + value: 1 + - key: readability-braces-around-statements.ShortStatementLines + value: 1 diff --git a/library/jni/Android.mk b/library/jni/Android.mk new file mode 100644 index 0000000..ce1be17 --- /dev/null +++ b/library/jni/Android.mk @@ -0,0 +1,15 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := lsplant +LOCAL_C_INCLUDES := $(LOCAL_PATH)/include +LOCAL_SRC_FILES := lsplant.cc +LOCAL_EXPORT_C_INCLUDES:= $(LOCAL_PATH)/include +LOCAL_STATIC_LIBRARIES := cxx dex_builder +LOCAL_LDLIBS := -llog +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,prefab/cxx) +include jni/external/dex_builder/Android.mk + diff --git a/library/jni/Application.mk b/library/jni/Application.mk new file mode 100644 index 0000000..98878a3 --- /dev/null +++ b/library/jni/Application.mk @@ -0,0 +1,14 @@ +APP_CFLAGS := -Wall -Wextra +APP_CFLAGS += -fno-stack-protector -fomit-frame-pointer +APP_CFLAGS += -Wno-builtin-macro-redefined -D__FILE__=__FILE_NAME__ -Wno-gnu-string-literal-operator-template +APP_CPPFLAGS := -std=c++20 +APP_CONLYFLAGS := -std=c18 +APP_STL := none + +ifneq ($(NDK_DEBUG),1) +APP_CFLAGS += -Oz -flto +APP_CFLAGS += -Wno-unused -Wno-unused-parameter -Werror +APP_CFLAGS += -fvisibility=hidden -fvisibility-inlines-hidden +APP_CFLAGS += -fno-unwind-tables -fno-asynchronous-unwind-tables +APP_LDFLAGS += -Wl,--exclude-libs,ALL -flto -Wl,--gc-sections -Wl,--strip-all +endif diff --git a/library/jni/art/instrumentation.hpp b/library/jni/art/instrumentation.hpp new file mode 100644 index 0000000..134f7b4 --- /dev/null +++ b/library/jni/art/instrumentation.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "common.hpp" +#include "runtime/art_method.hpp" + +namespace lsplant::art { + +class Instrumentation { + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art15instrumentation15Instrumentation21UpdateMethodsCodeImplEPNS_9ArtMethodEPKv", + void, UpdateMethodsCode, + (Instrumentation * thiz, ArtMethod * art_method, const void *quick_code), { + if (IsHooked(art_method)) [[unlikely]] { + LOGD("Skip update method code for hooked method %s", + art_method->PrettyMethod().c_str()); + return; + } else { + backup(thiz, art_method, quick_code); + } + }); + +public: + static bool Init(const HookHandler &handler) { + if (!HookSyms(handler, UpdateMethodsCode)) { + return false; + } + return true; + } +}; + +} diff --git a/library/jni/art/mirror/class.hpp b/library/jni/art/mirror/class.hpp new file mode 100644 index 0000000..f91aa5a --- /dev/null +++ b/library/jni/art/mirror/class.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art { + +namespace dex { +class ClassDef { + +}; +} + +namespace mirror { + +class Class { +private: + CREATE_MEM_FUNC_SYMBOL_ENTRY(const char *, GetDescriptor, Class *thiz, std::string *storage) { + if (GetDescriptorSym) [[likely]] + return GetDescriptor(thiz, storage); + else + return ""; + } + + CREATE_MEM_FUNC_SYMBOL_ENTRY(const dex::ClassDef *, GetClassDef, Class *thiz) { + if (GetClassDefSym) [[likely]] + return GetClassDefSym(thiz); + return nullptr; + } + +public: + static bool Init(JNIEnv *env, const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(GetDescriptor, + "_ZN3art6mirror5Class13GetDescriptorEPNSt3__112" + "basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEE")) { + return false; + } + if (!RETRIEVE_MEM_FUNC_SYMBOL(GetClassDef, "_ZN3art6mirror5Class11GetClassDefEv")) { + return false; + } + + auto clazz = JNI_FindClass(env, "java/lang/Class"); + if (!clazz) { + LOGE("Failed to find Class"); + return false; + } + + if (class_status = JNI_GetFieldID(env, clazz, "status", "I"); !class_status) { + LOGE("Failed to find status"); + return false; + } + + int sdk_int = GetAndroidApiLevel(); + + if (sdk_int >= __ANDROID_API_P__) { + is_unsigned = true; + } + + return true; + } + + const char *GetDescriptor(std::string *storage) { + if (GetDescriptorSym) { + return GetDescriptor(storage); + } + return ""; + } + + std::string GetDescriptor() { + std::string storage; + return GetDescriptor(&storage); + } + + const dex::ClassDef *GetClassDef() { + if (GetClassDefSym) + return GetClassDef(this); + return nullptr; + } + + static int GetStatus(JNIEnv *env, jclass clazz) { + int status = JNI_GetLongField(env, clazz, class_status); + return is_unsigned ? static_cast(status) >> (32 - 4) : status; + } + + static bool IsInitialized(JNIEnv *env, jclass clazz) { + return is_unsigned ? GetStatus(env, clazz) >= 14 : GetStatus(env, clazz) == 11; + } + + static Class *FromReflectedClass(JNIEnv *, jclass clazz) { + return reinterpret_cast(art::Thread::Current()->DecodeJObject(clazz)); + } + +private: + inline static jfieldID class_status = nullptr; + inline static bool is_unsigned = false; +}; + +} +} diff --git a/library/jni/art/runtime.hpp b/library/jni/art/runtime.hpp new file mode 100644 index 0000000..6a1be22 --- /dev/null +++ b/library/jni/art/runtime.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art { + +class Runtime { +private: + inline static Runtime *instance_; + + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, SetJavaDebuggable, Runtime *thiz, bool value) { + if (SetJavaDebuggableSym) [[likely]] { + SetJavaDebuggableSym(thiz, value); + } + } + +public: + inline static Runtime *Current() { + return instance_; + } + + void SetJavaDebuggable(bool value) { + SetJavaDebuggable(this, value); + } + + static bool Init(const HookHandler &handler) { + Runtime *instance = nullptr; + if (!RETRIEVE_FIELD_SYMBOL(instance, "_ZN3art7Runtime9instance_E")) { + return false; + } + instance_ = *reinterpret_cast(instance); + LOGD("_ZN3art7Runtime9instance_E = %p", instance_); + if (!RETRIEVE_MEM_FUNC_SYMBOL(SetJavaDebuggable, "_ZN3art7Runtime17SetJavaDebuggableEb")) { + return false; + } + return false; + } +}; + +} diff --git a/library/jni/art/runtime/art_method.hpp b/library/jni/art/runtime/art_method.hpp new file mode 100644 index 0000000..db4721c --- /dev/null +++ b/library/jni/art/runtime/art_method.hpp @@ -0,0 +1,180 @@ +#pragma once + +#include "common.hpp" +#include "art/mirror/class.hpp" + +namespace lsplant::art { + +class ArtMethod { + CREATE_MEM_FUNC_SYMBOL_ENTRY(std::string, PrettyMethod, ArtMethod *thiz, bool with_signature) { + if (thiz == nullptr) [[unlikely]] return "null"; + else if (PrettyMethodSym) [[likely]] return PrettyMethodSym(thiz, with_signature); + else return "null sym"; + } + +public: + void SetNonCompilable() { + auto access_flags = GetAccessFlags(); + access_flags |= kAccCompileDontBother; + access_flags &= ~kAccPreCompiled; + SetAccessFlags(access_flags); + } + + void SetNonIntrinsic() { + auto access_flags = GetAccessFlags(); + access_flags &= ~kAccFastInterpreterToInterpreterInvoke; + SetAccessFlags(access_flags); + } + + void SetPrivate() { + auto access_flags = GetAccessFlags(); + if (!(access_flags & kAccStatic)) { + access_flags |= kAccPrivate; + access_flags &= ~kAccProtected; + access_flags &= ~kAccPublic; + SetAccessFlags(access_flags); + } + } + + bool IsStatic() { + return GetAccessFlags() & kAccStatic; + } + + void CopyFrom(const ArtMethod *other) { + memcpy(this, other, art_method_size); + } + + void SetEntryPoint(void *entry_point) { + *reinterpret_cast(reinterpret_cast(this) + + entry_point_offset) = entry_point; + } + + void *GetEntryPoint() { + return *reinterpret_cast(reinterpret_cast(this) + + entry_point_offset); + } + + void *GetData() { + return *reinterpret_cast(reinterpret_cast(this) + + data_offset); + } + + uint32_t GetAccessFlags() { + return (reinterpret_cast *>( + reinterpret_cast(this) + access_flags_offset))->load( + std::memory_order_relaxed); + } + + void SetAccessFlags(uint32_t flags) { + return (reinterpret_cast *>( + reinterpret_cast(this) + access_flags_offset))->store( + flags, std::memory_order_relaxed); + } + + std::string PrettyMethod(bool with_signature = true) { + return PrettyMethod(this, with_signature); + } + + mirror::Class *GetDeclaringClass() { + return *reinterpret_cast(reinterpret_cast(this) + + declaring_class_offset); + } + + static art::ArtMethod *FromReflectedMethod(JNIEnv *env, jobject method) { + return reinterpret_cast(JNI_GetLongField(env, method, art_method_field)); + } + + static bool Init(JNIEnv *env, const lsplant::InitInfo info) { + auto executable = JNI_FindClass(env, "java/lang/reflect/Executable"); + if (!executable) { + LOGE("Failed to found Executable"); + return false; + } + + if (art_method_field = JNI_GetFieldID(env, executable, "artMethod", + "J"); !art_method_field) { + LOGE("Failed to find artMethod field"); + return false; + } + + auto throwable = JNI_FindClass(env, "java/lang/Throwable"); + if (!throwable) { + LOGE("Failed to found Executable"); + return false; + } + auto clazz = JNI_FindClass(env, "java/lang/Class"); + static_assert(std::is_same_v); + jmethodID get_declared_constructors = JNI_GetMethodID(env, clazz, "getDeclaredConstructors", + "()[Ljava/lang/reflect/Constructor;"); + auto constructors = JNI_Cast( + JNI_CallObjectMethod(env, clazz, get_declared_constructors)); + auto length = JNI_GetArrayLength(env, constructors); + if (length < 2) { + LOGE("Throwable has less than 2 constructors"); + return false; + } + auto *first = FromReflectedMethod(env, + JNI_GetObjectArrayElement(env, constructors, 0).get()); + auto *second = FromReflectedMethod(env, + JNI_GetObjectArrayElement(env, constructors, 0).get()); + art_method_size = reinterpret_cast(second) - reinterpret_cast(first); + LOGD("ArtMethod size: %zu", art_method_size); + + if (RoundUpTo(4 * 4 + 2 * 2, kPointerSize) + kPointerSize * 3 < art_method_size) { + LOGW("ArtMethod size exceeds maximum assume. There may be something wrong."); + } + + entry_point_offset = art_method_size - sizeof(void *); + LOGD("ArtMethod::entrypoint offset: %zu", entry_point_offset); + + data_offset = entry_point_offset - sizeof(void *); + LOGD("ArtMethod::data offset: %zu", data_offset); + + declaring_class_offset = 0U; + LOGD("ArtMethod::declaring_class offset: %zu", declaring_class_offset); + + access_flags_offset = 4U; + LOGD("ArtMethod::access_flags offset: %zu", access_flags_offset); + auto sdk_int = GetAndroidApiLevel(); + + if (sdk_int < __ANDROID_API_R__) kAccPreCompiled = 0; + if (sdk_int < __ANDROID_API_Q__) kAccFastInterpreterToInterpreterInvoke = 0; + + get_method_shorty_symbol = GetArtSymbol( + info.art_symbol_resolver, + "_ZN3artL15GetMethodShortyEP7_JNIEnvP10_jmethodID"); + if (!get_method_shorty_symbol) return false; + return true; + } + + static const char *GetMethodShorty(_JNIEnv *env, _jmethodID *method) { + if (get_method_shorty_symbol) [[likely]] return get_method_shorty_symbol(env, method); + return nullptr; + } + + static size_t GetEntryPointOffset() { + return entry_point_offset; + } + + constexpr static uint32_t kAccPublic = 0x0001; // class, field, method, ic + constexpr static uint32_t kAccPrivate = 0x0002; // field, method, ic + constexpr static uint32_t kAccProtected = 0x0004; // field, method, ic + constexpr static uint32_t kAccStatic = 0x0008; // field, method, ic + constexpr static uint32_t kAccNative = 0x0100; // method + +private: + inline static jfieldID art_method_field = nullptr; + inline static size_t art_method_size = 0; + inline static size_t entry_point_offset = 0; + inline static size_t data_offset = 0; + inline static size_t declaring_class_offset = 0; + inline static size_t access_flags_offset = 0; + inline static uint32_t kAccFastInterpreterToInterpreterInvoke = 0x40000000; + inline static uint32_t kAccPreCompiled = 0x00200000; + inline static uint32_t kAccCompileDontBother = 0x02000000; + + inline static const char * + (*get_method_shorty_symbol)(_JNIEnv *env, _jmethodID *method) = nullptr; +}; + +} diff --git a/library/jni/art/runtime/class_linker.hpp b/library/jni/art/runtime/class_linker.hpp new file mode 100644 index 0000000..ad2e5a0 --- /dev/null +++ b/library/jni/art/runtime/class_linker.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include "art/runtime/art_method.hpp" +#include "art/mirror/class.hpp" +#include "art/thread.hpp" +#include "common.hpp" + +namespace lsplant::art { + +class ClassLinker { + +private: + CREATE_MEM_FUNC_SYMBOL_ENTRY( + void, SetEntryPointsToInterpreter, ClassLinker *thiz, ArtMethod *art_method) { + if (SetEntryPointsToInterpreterSym) [[likely]] { + SetEntryPointsToInterpreterSym(thiz, art_method); + } + } + + [[gnu::always_inline]] + static void MaybeDelayHook(mirror::Class *clazz) { + const auto *class_def = clazz->GetClassDef(); + bool should_intercept = class_def && IsPending(class_def); + if (should_intercept) [[unlikely]] { + LOGD("Pending hook for %p (%s)", clazz, clazz->GetDescriptor().c_str()); + OnPending(class_def); + } + } + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker22FixupStaticTrampolinesENS_6ObjPtrINS_6mirror5ClassEEE", + void, FixupStaticTrampolines, (ClassLinker * thiz, mirror::Class * clazz), { + backup(thiz, clazz); + MaybeDelayHook(clazz); + }); + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker22FixupStaticTrampolinesEPNS_6ThreadENS_6ObjPtrINS_6mirror5ClassEEE", + void, FixupStaticTrampolinesWithThread, + (ClassLinker * thiz, Thread * self, mirror::Class * clazz), { + backup(thiz, self, clazz); + MaybeDelayHook(clazz); + }); + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker20MarkClassInitializedEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEE", + void*, MarkClassInitialized, (ClassLinker * thiz, Thread * self, uint32_t * clazz_ptr), + { + void *result = backup(thiz, self, clazz_ptr); + auto clazz = reinterpret_cast(*clazz_ptr); + MaybeDelayHook(clazz); + return result; + }); + + CREATE_HOOK_STUB_ENTRY( + "_ZN3art11ClassLinker30ShouldUseInterpreterEntrypointEPNS_9ArtMethodEPKv", + bool, ShouldUseInterpreterEntrypoint, (ArtMethod * art_method, const void *quick_code), + { + if (quick_code != nullptr && + (IsHooked(art_method) || IsPending(art_method))) [[unlikely]] { + return false; + } + return backup(art_method, quick_code); + }); + + CREATE_FUNC_SYMBOL_ENTRY(void, art_quick_to_interpreter_bridge, void*) {} + + CREATE_FUNC_SYMBOL_ENTRY(void, art_quick_generic_jni_trampoline, void*) {} + + CREATE_HOOK_STUB_ENTRY( + "_ZN3art11interpreter29ShouldStayInSwitchInterpreterEPNS_9ArtMethodE", + bool, ShouldStayInSwitchInterpreter, (ArtMethod * art_method), { + if (IsHooked(art_method) || IsPending(art_method)) [[unlikely]] { + return false; + } + return backup(art_method); + }); + +public: + static bool Init(const HookHandler &handler) { + int api_level = GetAndroidApiLevel(); + + if (!RETRIEVE_MEM_FUNC_SYMBOL(SetEntryPointsToInterpreter, + "_ZNK3art11ClassLinker27SetEntryPointsToInterpreterEPNS_9ArtMethodE")) { + return false; + } + + if (!HookSyms(handler, ShouldUseInterpreterEntrypoint, ShouldStayInSwitchInterpreter)) { + return false; + } + + if (api_level >= __ANDROID_API_R__) { + // In android R, FixupStaticTrampolines won't be called unless it's marking it as + // visiblyInitialized. + // So we miss some calls between initialized and visiblyInitialized. + // Therefore we hook the new introduced MarkClassInitialized instead + // This only happens on non-x86 devices + if (!HookSyms(handler, MarkClassInitialized) || + !HookSyms(handler, FixupStaticTrampolinesWithThread, FixupStaticTrampolines)) { + return false; + } + } else { + if (!HookSyms(handler, FixupStaticTrampolines)) { + return false; + } + } + + // MakeInitializedClassesVisiblyInitialized will cause deadlock + // IsQuickToInterpreterBridge is inlined + // So we use GetSavedEntryPointOfPreCompiledMethod instead + + if (!RETRIEVE_FUNC_SYMBOL(art_quick_to_interpreter_bridge, + "art_quick_to_interpreter_bridge")) { + return false; + } + if (!RETRIEVE_FUNC_SYMBOL(art_quick_generic_jni_trampoline, + "art_quick_generic_jni_trampoline")) { + return false; + } + + LOGD("art_quick_to_interpreter_bridge = %p", art_quick_to_interpreter_bridgeSym); + LOGD("art_quick_generic_jni_trampoline = %p", art_quick_generic_jni_trampolineSym); + return true; + } + + [[gnu::always_inline]] + static bool SetEntryPointsToInterpreter(ArtMethod *art_method) { + if (art_quick_to_interpreter_bridgeSym && art_quick_generic_jni_trampolineSym) [[likely]] { + if (art_method->GetAccessFlags() & ArtMethod::kAccNative) [[unlikely]] { + art_method->SetEntryPoint( + reinterpret_cast(art_quick_generic_jni_trampolineSym)); + } else { + art_method->SetEntryPoint( + reinterpret_cast(art_quick_to_interpreter_bridgeSym)); + } + return true; + } + if (SetEntryPointsToInterpreterSym) [[likely]] { + SetEntryPointsToInterpreter(nullptr, art_method); + return true; + } + return false; + } + +}; +} diff --git a/library/jni/art/runtime/gc/collector_type.hpp b/library/jni/art/runtime/gc/collector_type.hpp new file mode 100644 index 0000000..40958a6 --- /dev/null +++ b/library/jni/art/runtime/gc/collector_type.hpp @@ -0,0 +1,42 @@ +#pragma once + +namespace lsplant::art::gc { +// Which types of collections are able to be performed. +enum CollectorType { + // No collector selected. + kCollectorTypeNone, + // Non concurrent mark-sweep. + kCollectorTypeMS, + // Concurrent mark-sweep. + kCollectorTypeCMS, + // Semi-space / mark-sweep hybrid, enables compaction. + kCollectorTypeSS, + // Heap trimming collector, doesn't do any actual collecting. + kCollectorTypeHeapTrim, + // A (mostly) concurrent copying collector. + kCollectorTypeCC, + // The background compaction of the concurrent copying collector. + kCollectorTypeCCBackground, + // Instrumentation critical section fake collector. + kCollectorTypeInstrumentation, + // Fake collector for adding or removing application image spaces. + kCollectorTypeAddRemoveAppImageSpace, + // Fake collector used to implement exclusion between GC and debugger. + kCollectorTypeDebugger, + // A homogeneous space compaction collector used in background transition + // when both foreground and background collector are CMS. + kCollectorTypeHomogeneousSpaceCompact, + // Class linker fake collector. + kCollectorTypeClassLinker, + // JIT Code cache fake collector. + kCollectorTypeJitCodeCache, + // Hprof fake collector. + kCollectorTypeHprof, + // Fake collector for installing/removing a system-weak holder. + kCollectorTypeAddRemoveSystemWeakHolder, + // Fake collector type for GetObjectsAllocated + kCollectorTypeGetObjectsAllocated, + // Fake collector type for ScopedGCCriticalSection + kCollectorTypeCriticalSection, +}; +} // namespace gc diff --git a/library/jni/art/runtime/gc/gc_cause.hpp b/library/jni/art/runtime/gc/gc_cause.hpp new file mode 100644 index 0000000..17ac51c --- /dev/null +++ b/library/jni/art/runtime/gc/gc_cause.hpp @@ -0,0 +1,44 @@ +#pragma once +namespace lsplant::art::gc { +// What caused the GC? +enum GcCause { + // Invalid GC cause used as a placeholder. + kGcCauseNone, + // GC triggered by a failed allocation. Thread doing allocation is blocked waiting for GC before + // retrying allocation. + kGcCauseForAlloc, + // A background GC trying to ensure there is free memory ahead of allocations. + kGcCauseBackground, + // An explicit System.gc() call. + kGcCauseExplicit, + // GC triggered for a native allocation when NativeAllocationGcWatermark is exceeded. + // (This may be a blocking GC depending on whether we run a non-concurrent collector). + kGcCauseForNativeAlloc, + // GC triggered for a collector transition. + kGcCauseCollectorTransition, + // Not a real GC cause, used when we disable moving GC (currently for GetPrimitiveArrayCritical). + kGcCauseDisableMovingGc, + // Not a real GC cause, used when we trim the heap. + kGcCauseTrim, + // Not a real GC cause, used to implement exclusion between GC and instrumentation. + kGcCauseInstrumentation, + // Not a real GC cause, used to add or remove app image spaces. + kGcCauseAddRemoveAppImageSpace, + // Not a real GC cause, used to implement exclusion between GC and debugger. + kGcCauseDebugger, + // GC triggered for background transition when both foreground and background collector are CMS. + kGcCauseHomogeneousSpaceCompact, + // Class linker cause, used to guard filling art methods with special values. + kGcCauseClassLinker, + // Not a real GC cause, used to implement exclusion between code cache metadata and GC. + kGcCauseJitCodeCache, + // Not a real GC cause, used to add or remove system-weak holders. + kGcCauseAddRemoveSystemWeakHolder, + // Not a real GC cause, used to prevent hprof running in the middle of GC. + kGcCauseHprof, + // Not a real GC cause, used to prevent GetObjectsAllocated running in the middle of GC. + kGcCauseGetObjectsAllocated, + // GC cause for the profile saver. + kGcCauseProfileSaver, +}; +} // namespace art diff --git a/library/jni/art/runtime/gc/scoped_gc_critical_section.hpp b/library/jni/art/runtime/gc/scoped_gc_critical_section.hpp new file mode 100644 index 0000000..8d91650 --- /dev/null +++ b/library/jni/art/runtime/gc/scoped_gc_critical_section.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "gc_cause.hpp" +#include "collector_type.hpp" +#include "art/thread.hpp" +#include "common.hpp" + + +namespace lsplant::art::gc { + +class GCCriticalSection { +private: + [[maybe_unused]] void *self_; + [[maybe_unused]] const char *section_name_; +}; + +class ScopedGCCriticalSection { + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, constructor, ScopedGCCriticalSection *thiz, Thread *self, + GcCause cause, CollectorType collector_type) { + if (thiz == nullptr) [[unlikely]] return; + if (constructorSym) [[likely]] + return constructorSym(thiz, self, cause, collector_type); + } + + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, destructor, ScopedGCCriticalSection *thiz) { + if (thiz == nullptr) [[unlikely]] return; + if (destructorSym) [[likely]] + return destructorSym(thiz); + } + +public: + ScopedGCCriticalSection(Thread *self, GcCause cause, CollectorType collector_type) { + constructor(this, self, cause, collector_type); + } + + ~ScopedGCCriticalSection() { + destructor(this); + } + + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(constructor, + "_ZN3art2gc23ScopedGCCriticalSectionC2EPNS_6ThreadENS0_7GcCauseENS0_13CollectorTypeE")) { + return false; + } + if (!RETRIEVE_MEM_FUNC_SYMBOL(destructor, "_ZN3art2gc23ScopedGCCriticalSectionD2Ev")) { + return false; + } + return true; + } + +private: + [[maybe_unused]] GCCriticalSection critical_section_; + [[maybe_unused]] const char *old_no_suspend_reason_; +}; +} diff --git a/library/jni/art/runtime/jit/jit_code_cache.hpp b/library/jni/art/runtime/jit/jit_code_cache.hpp new file mode 100644 index 0000000..26976de --- /dev/null +++ b/library/jni/art/runtime/jit/jit_code_cache.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art::jit { +class JitCodeCache { + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, MoveObsoleteMethod, JitCodeCache *thiz, + ArtMethod * old_method, ArtMethod * new_method) { + if (MoveObsoleteMethodSym) + [[likely]] MoveObsoleteMethodSym(thiz, old_method, new_method); + } + + CREATE_MEM_HOOK_STUB_ENTRY( + "_ZN3art3jit12JitCodeCache19GarbageCollectCacheEPNS_6ThreadE", + void, GarbageCollectCache, (JitCodeCache * thiz, Thread * self), { + LOGD("Before jit cache gc, moving hooked methods"); + for (auto[target, backup] : GetJitMovements()) { + MoveObsoleteMethod(thiz, target, backup); + } + backup(thiz, self); + }); + +public: + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(MoveObsoleteMethod, + "_ZN3art3jit12JitCodeCache18MoveObsoleteMethodEPNS_9ArtMethodES3_")) { + return false; + } + if (!HookSyms(handler, GarbageCollectCache)) { + return false; + } + return true; + } +}; +} diff --git a/library/jni/art/thread.hpp b/library/jni/art/thread.hpp new file mode 100644 index 0000000..95f54fb --- /dev/null +++ b/library/jni/art/thread.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art { + +class Thread { + struct ObjPtr { + void *data; + }; + + CREATE_MEM_FUNC_SYMBOL_ENTRY(ObjPtr, DecodeJObject, Thread *thiz, jobject obj) { + if (DecodeJObjectSym) + return DecodeJObjectSym(thiz, obj); + else + return { .data=nullptr }; + } + + CREATE_FUNC_SYMBOL_ENTRY(Thread *, CurrentFromGdb) { + if (CurrentFromGdbSym) [[likely]] + return CurrentFromGdbSym(); + else + return nullptr; + } + +public: + static Thread *Current() { + return CurrentFromGdb(); + } + + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(DecodeJObject, + "_ZNK3art6Thread13DecodeJObjectEP8_jobject")) { + return false; + } + if (!RETRIEVE_FUNC_SYMBOL(CurrentFromGdb, + "_ZN3art6Thread14CurrentFromGdbEv")) { + return false; + } + return true; + } + + void *DecodeJObject(jobject obj) { + if (DecodeJObjectSym) [[likely]] { + return DecodeJObject(this, obj).data; + } + return nullptr; + } +}; +} diff --git a/library/jni/art/thread_list.hpp b/library/jni/art/thread_list.hpp new file mode 100644 index 0000000..bfcd788 --- /dev/null +++ b/library/jni/art/thread_list.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "common.hpp" + +namespace lsplant::art::thread_list { + +class ScopedSuspendAll { + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, constructor, ScopedSuspendAll *thiz, const char *cause, + bool long_suspend) { + if (thiz == nullptr) [[unlikely]] return; + if (constructorSym) [[likely]] + return constructorSym(thiz, cause, long_suspend); + } + + CREATE_MEM_FUNC_SYMBOL_ENTRY(void, destructor, ScopedSuspendAll *thiz) { + if (thiz == nullptr) [[unlikely]] return; + if (destructorSym) [[likely]] + return destructorSym(thiz); + } + +public: + ScopedSuspendAll(const char *cause, bool long_suspend) { + constructor(this, cause, long_suspend); + } + + ~ScopedSuspendAll() { + destructor(this); + } + + static bool Init(const HookHandler &handler) { + if (!RETRIEVE_MEM_FUNC_SYMBOL(constructor, "_ZN3art16ScopedSuspendAllC2EPKcb")) { + return false; + } + if (!RETRIEVE_MEM_FUNC_SYMBOL(destructor, "_ZN3art16ScopedSuspendAllD2Ev")) { + return false; + } + return true; + } +}; + +} + diff --git a/library/jni/common.hpp b/library/jni/common.hpp new file mode 100644 index 0000000..5909c7e --- /dev/null +++ b/library/jni/common.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "logging.hpp" +#include "utils/hook_helper.hpp" +#include "lsplant.hpp" + +namespace lsplant { + +enum class Arch { + kArm, + kArm64, + kX86, + kX8664, +}; + + +consteval inline Arch GetArch() { +#if defined(__i386__) + return Arch::kX86; +#elif defined(__x86_64__) + return Arch::kX8664; +#elif defined(__arm__) + return Arch::kArm; +#elif defined(__aarch64__) + return Arch::kArm64; +#else +# error "unsupported architecture" +#endif +} + +inline static constexpr auto kArch = GetArch(); + +template +constexpr inline auto RoundUpTo(T v, size_t size) { + return v + size - 1 - ((v + size - 1) & (size - 1)); +} + +inline auto GetAndroidApiLevel() { + static auto kApiLevel = []() { + std::array prop_value; + __system_property_get("ro.build.version.sdk", prop_value.data()); + int base = atoi(prop_value.data()); + __system_property_get("ro.build.version.preview_sdk", prop_value.data()); + return base + atoi(prop_value.data()); + }(); + return kApiLevel; +} + +inline static constexpr auto kPointerSize = sizeof(void *); + +template +inline T GetArtSymbol(const std::function &resolver, + std::string_view symbol) requires(std::is_pointer_v) { + if (auto *result = resolver(symbol); result) { + return reinterpret_cast(result); + } else { + LOGW("Failed to find symbol %*s", static_cast(symbol.length()), symbol.data()); + return nullptr; + } +} + +namespace art { +class ArtMethod; +namespace dex { +class ClassDef; +} +} + +namespace { +// target, backup +inline std::unordered_map hooked_methods_; +inline std::shared_mutex hooked_methods_lock_; + +inline std::list> jit_movements_; +inline std::shared_mutex jit_movements_lock_; + +inline std::unordered_map>> pending_classes_; +inline std::shared_mutex pending_classes_lock_; + +inline std::unordered_set pending_methods_; +inline std::shared_mutex pending_methods_lock_; +} + +inline bool IsHooked(art::ArtMethod *art_method) { + std::shared_lock lk(hooked_methods_lock_); + return hooked_methods_.contains(art_method); +} + +inline bool IsPending(art::ArtMethod *art_method) { + std::shared_lock lk(pending_methods_lock_); + return pending_methods_.contains(art_method); +} + +inline bool IsPending(const art::dex::ClassDef *class_def) { + std::shared_lock lk(pending_classes_lock_); + return pending_classes_.contains(class_def); +} + +inline std::list> GetJitMovements() { + std::unique_lock lk(jit_movements_lock_); + return std::move(jit_movements_); +} + +inline void RecordHooked(art::ArtMethod *target, art::ArtMethod *backup) { + std::unique_lock lk(hooked_methods_lock_); + hooked_methods_.emplace(target, backup); +} + +inline void RecordJitMovement(art::ArtMethod *target, art::ArtMethod *backup) { + std::unique_lock lk(jit_movements_lock_); + jit_movements_.emplace_back(target, backup); +} + +inline void +RecordPending(const art::dex::ClassDef *class_def, art::ArtMethod *target, art::ArtMethod *hook, + art::ArtMethod *backup) { + { + std::unique_lock lk(pending_methods_lock_); + pending_methods_.emplace(target); + } + std::unique_lock lk(pending_classes_lock_); + pending_classes_[class_def].emplace_back(std::make_tuple(target, hook, backup)); +} + +void OnPending(art::ArtMethod *target, art::ArtMethod *hook, art::ArtMethod *backup); + +inline void OnPending(const art::dex::ClassDef *class_def) { + { + std::shared_lock lk(pending_classes_lock_); + if (!pending_classes_.contains(class_def)) return; + } + typename decltype(pending_classes_)::value_type::second_type set; + { + std::unique_lock lk(pending_classes_lock_); + auto it = pending_classes_.find(class_def); + if (it == pending_classes_.end()) return; + set = std::move(it->second); + pending_classes_.erase(it); + } + for (auto&[target, hook, backup]: set) { + { + std::unique_lock mlk(pending_methods_lock_); + pending_methods_.erase(target); + } + OnPending(target, hook, backup); + } +} +} diff --git a/library/jni/external/dex_builder b/library/jni/external/dex_builder new file mode 160000 index 0000000..9d202f4 --- /dev/null +++ b/library/jni/external/dex_builder @@ -0,0 +1 @@ +Subproject commit 9d202f44c90489ef4759328a3f55a56effdc9105 diff --git a/library/jni/include/lsplant.hpp b/library/jni/include/lsplant.hpp new file mode 100644 index 0000000..83213e8 --- /dev/null +++ b/library/jni/include/lsplant.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace lsplant { + +struct InitInfo { + using InlineHookFunType = std::function; + using InlineUnhookFunType = std::function; + using ArtSymbolResolver = std::function; + + InlineHookFunType inline_hooker; + InlineUnhookFunType inline_unhooker; + ArtSymbolResolver art_symbol_resolver; +}; + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool Init(JNIEnv *env, const InitInfo &info); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +jmethodID +Hook(JNIEnv *env, jmethodID target_method, jobject hooker_object, jmethodID callback_method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool UnHook(JNIEnv *env, jmethodID target_method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool IsHooked(JNIEnv *env, jmethodID method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +bool Deoptimize(JNIEnv *env, jmethodID method); + +[[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] +void *GetNativeFunction(JNIEnv *env, jmethodID method); + +} // namespace lsplant diff --git a/library/jni/include/utils/hook_helper.hpp b/library/jni/include/utils/hook_helper.hpp new file mode 100644 index 0000000..1513dd1 --- /dev/null +++ b/library/jni/include/utils/hook_helper.hpp @@ -0,0 +1,197 @@ +#pragma once + +#include "logging.hpp" +#include "jni_helper.hpp" +#include "hook_helper.hpp" +#include + +#define CONCATENATE(a, b) a##b + +#define CREATE_HOOK_STUB_ENTRY(SYM, RET, FUNC, PARAMS, DEF) \ + inline static struct : public lsplant::Hooker{ \ + inline static RET replace PARAMS DEF \ + } FUNC + +#define CREATE_MEM_HOOK_STUB_ENTRY(SYM, RET, FUNC, PARAMS, DEF) \ + inline static struct : public lsplant::MemHooker{ \ + inline static RET replace PARAMS DEF \ + } FUNC + +#define RETRIEVE_FUNC_SYMBOL(name, ...) \ + (name##Sym = reinterpret_cast( \ + lsplant::Dlsym(handler, __VA_ARGS__))) + +#define RETRIEVE_MEM_FUNC_SYMBOL(name, ...) \ + (name##Sym = reinterpret_cast( \ + lsplant::Dlsym(handler, __VA_ARGS__))) + +#define RETRIEVE_FIELD_SYMBOL(name, ...) \ + (name = reinterpret_cast(lsplant::Dlsym(handler, __VA_ARGS__))) + +#define CREATE_FUNC_SYMBOL_ENTRY(ret, func, ...) \ + typedef ret (*func##Type)(__VA_ARGS__); \ + inline static ret (*func##Sym)(__VA_ARGS__); \ + inline static ret func(__VA_ARGS__) + +#define CREATE_MEM_FUNC_SYMBOL_ENTRY(ret, func, thiz, ...) \ + using func##Type = lsplant::MemberFunction; \ + inline static func##Type func##Sym; \ + inline static ret func(thiz, ## __VA_ARGS__) + +namespace lsplant { + +using HookHandler = InitInfo; + +template +struct tstring : public std::integer_sequence { + inline constexpr static const char *c_str() { + return str_; + } + + inline constexpr operator std::string_view() const { + return { c_str(), sizeof...(chars) }; + } + +private: + constexpr static char str_[]{ chars..., '\0' }; +}; + +template +inline constexpr tstring operator ""_tstr() { + return {}; +} + + +template +inline constexpr tstring +operator+(const tstring &, const tstring &) { + return {}; +} + +template +inline constexpr auto operator+(const std::string &a, const tstring &) { + char b[]{ as..., '\0' }; + return a + b; +} + +template +inline constexpr auto operator+(const tstring &, const std::string &b) { + char a[]{ as..., '\0' }; + return a + b; +} + +inline void *Dlsym(const HookHandler &handle, const char *name) { + return handle.art_symbol_resolver(name); +} + +template +requires (std::is_same_v || std::is_same_v) +inline static auto memfun_cast(Return (*func)(T *, Args...)) { + union { + Return (Class::*f)(Args...); + + struct { + decltype(func) p; + std::ptrdiff_t adj; + } data; + } u{ .data = { func, 0 }}; + static_assert(sizeof(u.f) == sizeof(u.data), "Try different T"); + return u.f; +} + +template T, typename Return, typename... Args> +inline auto memfun_cast(Return (*func)(T *, Args...)) { + return memfun_cast(func); +} + +template +class MemberFunction; + +template +class MemberFunction { + using SelfType = MemberFunction; + using ThisType = std::conditional_t, SelfType, This>; + using MemFunType = Return(ThisType::*)(Args...); +public: + using FunType = Return (*)(This *, Args...); +private: + MemFunType f_ = nullptr; +public: + MemberFunction() = default; + + MemberFunction(FunType f) : f_(memfun_cast(f)) {} + + MemberFunction(MemFunType f) : f_(f) {} + + Return operator()(This *thiz, Args... args) { + return (reinterpret_cast(thiz)->*f_)(std::forward(args)...); + } + + inline operator bool() { + return f_ != nullptr; + } +}; + +// deduction guide +template +MemberFunction(Return(*f)(This *, Args...)) -> MemberFunction; + +template +MemberFunction(Return(This::*f)(Args...)) -> MemberFunction; + +template +struct Hooker; + +template +struct Hooker> { + inline static Ret (*backup)(Args...) = nullptr; + + inline static constexpr std::string_view sym = tstring{}; +}; + +template +struct MemHooker; +template +struct MemHooker> { + inline static MemberFunction backup; + inline static constexpr std::string_view sym = tstring{}; +}; + +template +concept HookerType = requires(T a) { + a.backup; + a.replace; +}; + +template +inline static bool HookSymNoHandle(const HookHandler &handler, void *original, T &arg) { + if (original) { + if constexpr(is_instance::value) { + void *backup = handler.inline_hooker(original, reinterpret_cast(arg.replace)); + arg.backup = reinterpret_cast(backup); + } else { + arg.backup = reinterpret_cast(handler.inline_hooker(original, + reinterpret_cast(arg.replace))); + } + return true; + } else { + return false; + } +} + +template +inline static bool HookSym(const HookHandler &handler, T &arg) { + auto original = handler.art_symbol_resolver(arg.sym); + return HookSymNoHandle(handler, original, arg); +} + +template +inline static bool HookSyms(const HookHandler &handle, T &first, Args &...rest) { + if (!(HookSym(handle, first) || ... || HookSym(handle, rest))) { + LOGW("Hook Fails: %*s", static_cast(first.sym.size()), first.sym.data()); + return false; + } + return true; +} + +} diff --git a/library/jni/include/utils/jni_helper.hpp b/library/jni/include/utils/jni_helper.hpp new file mode 100644 index 0000000..d220296 --- /dev/null +++ b/library/jni/include/utils/jni_helper.hpp @@ -0,0 +1,424 @@ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection" +#pragma once + +#include +#include +#include "logging.hpp" + +#define DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&) = delete; \ + void operator=(const TypeName&) = delete + +namespace lsplant { +template class> +struct is_instance : public std::false_type { +}; + +template class U> +struct is_instance, U> : public std::true_type { +}; + +template +concept JObject = std::is_base_of_v, std::remove_pointer_t>; + +template +class ScopedLocalRef { +public: + using BaseType [[maybe_unused]] = T; + + ScopedLocalRef(JNIEnv *env, T localRef) : env_(env), local_ref_(localRef) { + } + + ScopedLocalRef(ScopedLocalRef &&s) noexcept: env_(s.env_), local_ref_(s.release()) { + } + + template + ScopedLocalRef(ScopedLocalRef &&s) noexcept: env_(s.env_), local_ref_((T) s.release()) { + } + + explicit ScopedLocalRef(JNIEnv *env) noexcept: env_(env), local_ref_(nullptr) { + } + + ~ScopedLocalRef() { + reset(); + } + + void reset(T ptr = nullptr) { + if (ptr != local_ref_) { + if (local_ref_ != nullptr) { + env_->DeleteLocalRef(local_ref_); + } + local_ref_ = ptr; + } + } + + [[nodiscard]] T release() { + T localRef = local_ref_; + local_ref_ = nullptr; + return localRef; + } + + T get() const { + return local_ref_; + } + + operator T() const { + return local_ref_; + } + + // We do not expose an empty constructor as it can easily lead to errors + // using common idioms, e.g.: + // ScopedLocalRef<...> ref; + // ref.reset(...); + // Move assignment operator. + ScopedLocalRef &operator=(ScopedLocalRef &&s) noexcept { + reset(s.release()); + env_ = s.env_; + return *this; + } + + operator bool() const { + return local_ref_; + } + + template + friend + class ScopedLocalRef; + + friend + class JUTFString; + +private: + JNIEnv *env_; + T local_ref_; + DISALLOW_COPY_AND_ASSIGN(ScopedLocalRef); +}; + + +class JNIScopeFrame { + JNIEnv *env_; +public: + JNIScopeFrame(JNIEnv *env, jint size) : env_(env) { + env_->PushLocalFrame(size); + } + + ~JNIScopeFrame() { + env_->PopLocalFrame(nullptr); + } +}; + +template +concept ScopeOrRaw = std::is_convertible_v || + (is_instance, ScopedLocalRef>::value && + std::is_convertible_v::BaseType, U>); + +template +concept ScopeOrClass = ScopeOrRaw; + +template +concept ScopeOrObject = ScopeOrRaw; + +inline ScopedLocalRef ClearException(JNIEnv *env) { + if (auto exception = env->ExceptionOccurred()) { + env->ExceptionClear(); + static jclass log = (jclass) env->NewGlobalRef(env->FindClass("android/util/Log")); + static jmethodID toString = env->GetStaticMethodID(log, "getStackTraceString", + "(Ljava/lang/Throwable;)Ljava/lang/String;"); + auto str = (jstring) env->CallStaticObjectMethod(log, toString, exception); + env->DeleteLocalRef(exception); + return { env, str }; + } + return { env, nullptr }; +} + +template +[[maybe_unused]] +inline auto UnwrapScope(T &&x) { + if constexpr(std::is_same_v, std::string_view>) + return x.data(); + else if constexpr(is_instance, ScopedLocalRef>::value) + return x.get(); + else return std::forward(x); +} + +template +[[maybe_unused]] +inline auto WrapScope(JNIEnv *env, T &&x) { + if constexpr(std::is_convertible_v) { + return ScopedLocalRef(env, std::forward(x)); + } else return x; +} + +template +[[maybe_unused]] +inline auto WrapScope(JNIEnv *env, std::tuple &&x, std::index_sequence) { + return std::make_tuple(WrapScope(env, std::forward(std::get(x)))...); +} + +template +[[maybe_unused]] +inline auto WrapScope(JNIEnv *env, std::tuple &&x) { + return WrapScope(env, std::forward>(x), + std::make_index_sequence()); +} + +inline auto JNI_NewStringUTF(JNIEnv *env, std::string_view sv) { + return ScopedLocalRef(env, env->NewStringUTF(sv.data())); +} + +class JUTFString { +public: + inline JUTFString(JNIEnv *env, jstring jstr) : JUTFString(env, jstr, nullptr) { + } + + inline JUTFString(const ScopedLocalRef &jstr) : JUTFString(jstr.env_, jstr.local_ref_, + nullptr) { + } + + inline JUTFString(JNIEnv *env, jstring jstr, const char *default_cstr) : env_(env), + jstr_(jstr) { + if (env_ && jstr_) cstr_ = env_->GetStringUTFChars(jstr, nullptr); + else cstr_ = default_cstr; + } + + inline operator const char *() const { return cstr_; } + + inline operator const std::string() const { return cstr_; } + + inline operator const bool() const { return cstr_ != nullptr; } + + inline auto get() const { return cstr_; } + + inline ~JUTFString() { + if (env_ && jstr_) env_->ReleaseStringUTFChars(jstr_, cstr_); + } + + JUTFString(JUTFString &&other) + : env_(std::move(other.env_)), jstr_(std::move(other.jstr_)), + cstr_(std::move(other.cstr_)) { + other.cstr_ = nullptr; + } + + JUTFString & + operator=(JUTFString &&other) { + if (&other != this) { + env_ = std::move(other.env_); + jstr_ = std::move(other.jstr_); + cstr_ = std::move(other.cstr_); + other.cstr_ = nullptr; + } + return *this; + } + +private: + JNIEnv *env_; + jstring jstr_; + const char *cstr_; + + JUTFString(const JUTFString &) = delete; + + JUTFString &operator=(const JUTFString &) = delete; +}; + + +template +requires(std::is_function_v) +[[maybe_unused]] +inline auto JNI_SafeInvoke(JNIEnv *env, Func JNIEnv::*f, Args &&... args) { + struct finally { + finally(JNIEnv *env) : env_(env) {} + + ~finally() { + if (auto exception = ClearException(env_)) { + LOGE("%s", JUTFString(env_, exception.get()).get()); + } + } + + JNIEnv *env_; + } _(env); + + if constexpr(!std::is_same_v(args)))... >>) + return WrapScope(env, (env->*f)(UnwrapScope(std::forward(args))...)); + else (env->*f)(UnwrapScope(std::forward(args))...); +} + +[[maybe_unused]] +inline auto JNI_FindClass(JNIEnv *env, std::string_view name) { + return JNI_SafeInvoke(env, &JNIEnv::FindClass, name); +} + +template +[[maybe_unused]] +inline auto JNI_GetObjectClass(JNIEnv *env, const Object &obj) { + return JNI_SafeInvoke(env, &JNIEnv::GetObjectClass, obj); +} + +template +[[maybe_unused]] +inline auto +JNI_GetFieldID(JNIEnv *env, const Class &clazz, std::string_view name, std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetFieldID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto +JNI_ToReflectedMethod(JNIEnv *env, const Class &clazz, jmethodID method, jboolean isStatic) { + return JNI_SafeInvoke(env, &JNIEnv::ToReflectedMethod, clazz, method, isStatic); +} + +template +[[maybe_unused]] +inline auto JNI_GetObjectField(JNIEnv *env, const Object &obj, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetObjectField, obj, fieldId); +} + +template +[[maybe_unused]] +inline auto JNI_GetLongField(JNIEnv *env, const Object &obj, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetLongField, obj, fieldId); +} + +template +[[maybe_unused]] +inline auto JNI_GetIntField(JNIEnv *env, const Object &obj, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetIntField, obj, fieldId); +} + +template +[[maybe_unused]] +inline auto +JNI_GetMethodID(JNIEnv *env, const Class &clazz, std::string_view name, std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetMethodID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto +JNI_CallObjectMethod(JNIEnv *env, const Object &obj, jmethodID method, Args &&... args) { + return JNI_SafeInvoke(env, &JNIEnv::CallObjectMethod, obj, method, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallIntMethod(JNIEnv *env, const Object &obj, jmethodID method, Args &&... args) { + return JNI_SafeInvoke(env, &JNIEnv::CallIntMethod, obj, method, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallLongMethod(JNIEnv *env, const Object &obj, Args &&... args) { + return JNI_SafeInvoke(env, &JNIEnv::CallLongMethod, obj, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallVoidMethod(JNIEnv *env, const Object &obj, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallVoidMethod, obj, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallBooleanMethod(JNIEnv *env, const Object &obj, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallBooleanMethod, obj, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto +JNI_GetStaticFieldID(JNIEnv *env, const Class &clazz, std::string_view name, std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticFieldID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto JNI_GetStaticObjectField(JNIEnv *env, const Class &clazz, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticObjectField, clazz, fieldId); +} + +template +[[maybe_unused]] +inline auto JNI_GetStaticIntField(JNIEnv *env, const Class &clazz, jfieldID fieldId) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticIntField, clazz, fieldId); +} + +template +[[maybe_unused]] +inline auto +JNI_GetStaticMethodID(JNIEnv *env, const Class &clazz, std::string_view name, + std::string_view sig) { + return JNI_SafeInvoke(env, &JNIEnv::GetStaticMethodID, clazz, name, sig); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticVoidMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticVoidMethod, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticObjectMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticObjectMethod, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticIntMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticIntMethod, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto JNI_CallStaticBooleanMethod(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::CallStaticBooleanMethod, clazz, + std::forward(args)...); +} + +template Array> +[[maybe_unused]] +inline auto JNI_GetArrayLength(JNIEnv *env, const Array &array) { + return JNI_SafeInvoke(env, &JNIEnv::GetArrayLength, array); +} + +template Array> +[[maybe_unused]] +inline auto JNI_GetObjectArrayElement(JNIEnv *env, const Array &array, jsize idx) { + return JNI_SafeInvoke(env, &JNIEnv::GetObjectArrayElement, array, idx); +} + +template +[[maybe_unused]] +inline auto JNI_NewObject(JNIEnv *env, const Class &clazz, Args &&...args) { + return JNI_SafeInvoke(env, &JNIEnv::NewObject, clazz, std::forward(args)...); +} + +template +[[maybe_unused]] +inline auto +JNI_RegisterNatives(JNIEnv *env, const Class &clazz, const JNINativeMethod *methods, jint size) { + return JNI_SafeInvoke(env, &JNIEnv::RegisterNatives, clazz, methods, size); +} + +template +[[maybe_unused]] +inline auto JNI_NewGlobalRef(JNIEnv *env, Object &&x) { + return (decltype(UnwrapScope(std::forward(x)))) env->NewGlobalRef( + UnwrapScope(std::forward(x))); +} + +template +[[maybe_unused]] +inline auto +JNI_Cast(ScopedLocalRef &&x)requires(std::is_convertible_v) { + return ScopedLocalRef(std::move(x)); +} + +} + +#undef DISALLOW_COPY_AND_ASSIGN + +#pragma clang diagnostic pop diff --git a/library/jni/logging.hpp b/library/jni/logging.hpp new file mode 100644 index 0000000..0363a15 --- /dev/null +++ b/library/jni/logging.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#ifndef LOG_TAG +#define LOG_TAG "LSPlant" +#endif + +#ifdef LOG_DISABLED +#define LOGD(...) +#define LOGV(...) +#define LOGI(...) +#define LOGW(...) +#define LOGE(...) +#else +#ifndef NDEBUG +#define LOGD(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) +#define LOGV(fmt, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) +#else +#define LOGD(...) +#define LOGV(...) +#endif +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) +#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) +#endif diff --git a/library/jni/lsplant.cc b/library/jni/lsplant.cc new file mode 100644 index 0000000..433f0de --- /dev/null +++ b/library/jni/lsplant.cc @@ -0,0 +1,566 @@ +#include "lsplant.hpp" + +#include +#include +#include +#include +#include +#include "utils/jni_helper.hpp" +#include "art/runtime/gc/scoped_gc_critical_section.hpp" +#include "art/runtime.hpp" +#include "art/thread.hpp" +#include "art/instrumentation.hpp" +#include "art/runtime/jit/jit_code_cache.hpp" +#include "art/runtime/art_method.hpp" +#include "art/thread_list.hpp" +#include "art/runtime/class_linker.hpp" +#include "external/dex_builder/dex_builder.h" +#include "common.hpp" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma ide diagnostic ignored "ConstantConditionsOC" +#pragma ide diagnostic ignored "UnreachableCode" +namespace lsplant { + +using art::ArtMethod; +using art::thread_list::ScopedSuspendAll; +using art::ClassLinker; +using art::mirror::Class; +using art::Runtime; +using art::Thread; +using art::Instrumentation; +using art::gc::ScopedGCCriticalSection; +using art::jit::JitCodeCache; + +namespace { +template +inline consteval auto operator ""_arr() { + return std::array{ chars... }; +} + +consteval inline auto GetTrampoline() { + if constexpr(kArch == Arch::kArm) { + return std::make_tuple( + "\x00\x00\x9f\xe5\x20\xf0\x90\xe5\x78\x56\x34\x12"_arr, + // NOLINTNEXTLINE + uint8_t{ 4u }, uintptr_t{ 8u }); + } + if constexpr(kArch == Arch::kArm64) { + return std::make_tuple( + "\x60\x00\x00\x58\x10\x00\x40\xf8\x00\x02\x1f\xd6\x78\x56\x34\x12\x78\x56\x34\x12"_arr, + // NOLINTNEXTLINE + uint16_t{ 5u }, uintptr_t{ 12u }); + } + if constexpr(kArch == Arch::kX86) { + return std::make_tuple( + "\xb8\x78\x56\x34\x12\xff\x70\x20\xc3"_arr, + // NOLINTNEXTLINE + uint8_t{ 7u }, uintptr_t{ 1u }); + } + if constexpr(kArch == Arch::kX8664) { + return std::make_tuple( + "\x48\xbf\x78\x56\x34\x12\x78\x56\x34\x12\xff\x77\x20\xc3"_arr, + // NOLINTNEXTLINE + uint8_t{ 12u }, uintptr_t{ 2u }); + } +} + +auto[trampoline, entry_point_offset, art_method_offset] = GetTrampoline(); + +jmethodID method_get_name = nullptr; +jmethodID method_get_declaring_class = nullptr; +jmethodID class_get_canonical_name = nullptr; +jmethodID class_get_class_loader = nullptr; +jmethodID class_is_proxy = nullptr; +jclass in_memory_class_loader = nullptr; +jmethodID in_memory_class_loader_init = nullptr; +jmethodID load_class = nullptr; + +bool InitJNI(JNIEnv *env) { + auto executable = JNI_FindClass(env, "java/lang/reflect/Executable"); + if (!executable) { + LOGE("Failed to found Executable"); + return false; + } + + if (method_get_name = JNI_GetMethodID(env, executable, "getName", + "()Ljava/lang/String;"); !method_get_name) { + LOGE("Failed to find getName method"); + return false; + } + if (method_get_declaring_class = JNI_GetMethodID(env, executable, "getDeclaringClass", + "()Ljava/lang/Class;"); !method_get_declaring_class) { + LOGE("Failed to find getName method"); + return false; + } + auto clazz = JNI_FindClass(env, "java/lang/Class"); + if (!clazz) { + LOGE("Failed to find Class"); + return false; + } + + if (class_get_class_loader = JNI_GetMethodID(env, clazz, "getClassLoader", + "()Ljava/lang/ClassLoader;"); + !class_get_class_loader) { + LOGE("Failed to find getClassLoader"); + return false; + } + + if (class_get_canonical_name = JNI_GetMethodID(env, clazz, "getCanonicalName", + "()Ljava/lang/String;"); + !class_get_canonical_name) { + LOGE("Failed to find getCanonicalName"); + return false; + } + + if (class_is_proxy = JNI_GetMethodID(env, clazz, "isProxy", "()Z"); !class_is_proxy) { + LOGE("Failed to find isProxy"); + return false; + } + + in_memory_class_loader = JNI_NewGlobalRef(env, JNI_FindClass(env, + "dalvik/system/InMemoryDexClassLoader")); + in_memory_class_loader_init = JNI_GetMethodID(env, in_memory_class_loader, "", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); + + load_class = JNI_GetMethodID(env, in_memory_class_loader, "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + if (!load_class) { + load_class = JNI_GetMethodID(env, in_memory_class_loader, "findClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + } + return true; +} + +inline void UpdateTrampoline(decltype(entry_point_offset) offset) { + *reinterpret_cast(trampoline.data() + + entry_point_offset) = offset; +} + +bool InitNative(JNIEnv *env, const HookHandler &handler) { + if (!handler.inline_hooker || !handler.inline_unhooker || !handler.art_symbol_resolver) + return false; + if (!Runtime::Init(handler)) { + LOGE("Failed to init runtime"); + return false; + } + if (!ArtMethod::Init(env, handler)) { + LOGE("Failed to init art method"); + return false; + } + UpdateTrampoline(ArtMethod::GetEntryPointOffset()); + if (!Thread::Init(handler)) { + LOGE("Failed to init thread"); + return false; + } + if (!ClassLinker::Init(handler)) { + LOGE("Failed to init class linker"); + return false; + } + if (!Class::Init(env, handler)) { + LOGE("failed to init mirror class"); + return false; + } + if (!Instrumentation::Init(handler)) { + LOGE("failed to init intrumentation"); + return false; + } + if (!ScopedSuspendAll::Init(handler)) { + LOGE("failed to init scoped suspend all"); + return false; + } + if (!ScopedGCCriticalSection::Init(handler)) { + LOGE("failed to init scoped gc critical section"); + return false; + } + if (!JitCodeCache::Init(handler)) { + LOGE("failed to init jit code cache"); + return false; + } + return true; +} + +std::tuple +BuildDex(JNIEnv *env, jobject class_loader, + std::string_view shorty, bool is_static, std::string_view method_name, + std::string_view hooker_class, std::string_view callback_name) { + // NOLINTNEXTLINE + using namespace startop::dex; + + if (shorty.empty()) { + LOGE("Invalid shorty"); + return { nullptr, nullptr, nullptr, nullptr }; + } + + DexBuilder dex_file; + + auto parameter_types = std::vector(); + parameter_types.reserve(shorty.size() - 1); + std::string storage; + auto return_type = + shorty[0] == 'L' ? TypeDescriptor::Object : TypeDescriptor::FromDescriptor( + shorty[0]); + if (!is_static) parameter_types.push_back(TypeDescriptor::Object); // this object + for (const char ¶m: shorty.substr(1)) { + parameter_types.push_back( + param == 'L' ? TypeDescriptor::Object : TypeDescriptor::FromDescriptor( + static_cast(param))); + } + + // TODO(yujincheng08): customize it + ClassBuilder cbuilder{ dex_file.MakeClass("LspHooker_") }; + // TODO(yujincheng08): customize it + cbuilder.set_source_file("LSP"); + + auto hooker_type = TypeDescriptor::FromClassname(hooker_class.data()); + + // TODO(yujincheng08): customize it + auto *hooker_field = cbuilder.CreateField("hooker", hooker_type) + .access_flags(dex::kAccStatic) + .Encode(); + + auto hook_builder{ cbuilder.CreateMethod( + method_name.data(), Prototype{ return_type, parameter_types }) }; + // allocate tmp first because of wide + auto tmp{ hook_builder.AllocRegister() }; + hook_builder.BuildConst(tmp, static_cast(parameter_types.size())); + auto hook_params_array{ hook_builder.AllocRegister() }; + hook_builder.BuildNewArray(hook_params_array, TypeDescriptor::Object, tmp); + for (size_t i = 0U, j = 0U; i < parameter_types.size(); ++i, ++j) { + hook_builder.BuildBoxIfPrimitive(Value::Parameter(j), parameter_types[i], + Value::Parameter(j)); + hook_builder.BuildConst(tmp, static_cast(i)); + hook_builder.BuildAput(Instruction::Op::kAputObject, hook_params_array, + Value::Parameter(j), tmp); + if (parameter_types[i].is_wide()) ++j; + } + auto handle_hook_method{ dex_file.GetOrDeclareMethod( + hooker_type, callback_name.data(), + Prototype{ TypeDescriptor::Object, TypeDescriptor::Object.ToArray() }) }; + hook_builder.AddInstruction( + Instruction::GetStaticObjectField(hooker_field->decl->orig_index, tmp)); + hook_builder.AddInstruction(Instruction::InvokeVirtualObject( + handle_hook_method.id, tmp, tmp, hook_params_array)); + if (return_type == TypeDescriptor::Void) { + hook_builder.BuildReturn(); + } else if (return_type.is_primitive()) { + auto box_type{ return_type.ToBoxType() }; + const ir::Type *type_def = dex_file.GetOrAddType(box_type); + hook_builder.AddInstruction( + Instruction::Cast(tmp, Value::Type(type_def->orig_index))); + hook_builder.BuildUnBoxIfPrimitive(tmp, box_type, tmp); + hook_builder.BuildReturn(tmp, false, return_type.is_wide()); + } else { + const ir::Type *type_def = dex_file.GetOrAddType(return_type); + hook_builder.AddInstruction( + Instruction::Cast(tmp, Value::Type(type_def->orig_index))); + hook_builder.BuildReturn(tmp, true); + } + auto *hook_method = hook_builder.Encode(); + + auto backup_builder{ + cbuilder.CreateMethod("backup", Prototype{ return_type, parameter_types }) }; + if (return_type == TypeDescriptor::Void) { + backup_builder.BuildReturn(); + } else if (return_type.is_wide()) { + LiveRegister zero = backup_builder.AllocRegister(); + LiveRegister zero_wide = backup_builder.AllocRegister(); + backup_builder.BuildConstWide(zero, 0); + backup_builder.BuildReturn(zero, /*is_object=*/false, true); + } else { + LiveRegister zero = backup_builder.AllocRegister(); + LiveRegister zero_wide = backup_builder.AllocRegister(); + backup_builder.BuildConst(zero, 0); + backup_builder.BuildReturn(zero, /*is_object=*/!return_type.is_primitive(), false); + } + auto *backup_method = backup_builder.Encode(); + + slicer::MemView image{ dex_file.CreateImage() }; + + auto *dex_buffer = env->NewDirectByteBuffer(const_cast(image.ptr()), image.size()); + auto my_cl = JNI_NewObject(env, in_memory_class_loader, in_memory_class_loader_init, + dex_buffer, class_loader); + env->DeleteLocalRef(dex_buffer); + + if (my_cl) { + auto target_class = JNI_Cast(JNI_CallObjectMethod(env, my_cl, load_class, + JNI_NewStringUTF(env, + "LspHooker_"))); + return { + target_class.release(), + JNI_GetStaticFieldID(env, target_class, hooker_field->decl->name->c_str(), + hooker_field->decl->type->Decl().data()), + JNI_GetStaticMethodID(env, target_class, hook_method->decl->name->c_str(), + hook_method->decl->prototype->Signature().data()), + JNI_GetStaticMethodID(env, target_class, backup_method->decl->name->c_str(), + backup_method->decl->prototype->Signature().data()), + }; + } + return { nullptr, nullptr, nullptr, nullptr }; +} + +static_assert(std::endian::native == std::endian::little, "Unsupported architecture"); + +union Trampoline { +public: + uintptr_t address; + unsigned count: 12; +}; + +static_assert(sizeof(Trampoline) == sizeof(uintptr_t), "Unsupported architecture"); +static_assert(std::atomic_uintptr_t::is_always_lock_free, "Unsupported architecture"); + +std::atomic_uintptr_t trampoline_pool{ 0 }; +std::atomic_flag trampoline_lock{ false }; +constexpr size_t kTrampolineSize = RoundUpTo(sizeof(trampoline), kPointerSize); +constexpr size_t kPageSize = 4096; // assume +constexpr size_t kTrampolineNumPerPage = kPageSize / kTrampolineSize; +constexpr uintptr_t kAddressMask = 0xFFFU; + +void *GenerateTrampolineFor(art::ArtMethod *hook) { + unsigned count; + uintptr_t address; + while (true) { + auto tl = Trampoline{ .address = trampoline_pool.fetch_add(1, std::memory_order_release) }; + count = tl.count; + address = tl.address & ~kAddressMask; + if (address == 0 || count >= kTrampolineNumPerPage) { + if (trampoline_lock.test_and_set(std::memory_order_acq_rel)) { + trampoline_lock.wait(true, std::memory_order_acquire); + continue; + } + address = reinterpret_cast(mmap(nullptr, kPageSize, + PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)); + if (address == reinterpret_cast(MAP_FAILED)) { + PLOGE("mmap trampoline"); + trampoline_lock.clear(std::memory_order_release); + trampoline_lock.notify_all(); + return nullptr; + } + count = 0; + tl.address = address; + tl.count = count + 1; + trampoline_pool.store(tl.address, std::memory_order_release); + trampoline_lock.clear(std::memory_order_release); + trampoline_lock.notify_all(); + + } + LOGV("trampoline: count = %u, address = %zx, target = %zx", count, address, + address + count * kTrampolineSize); + address = address + count * kTrampolineSize; + break; + } + auto *address_ptr = reinterpret_cast(address); + std::memcpy(address_ptr, trampoline.data(), trampoline.size()); + + *reinterpret_cast(address_ptr + art_method_offset) = hook; + + __builtin___clear_cache(address_ptr, reinterpret_cast(address + trampoline.size())); + + return address_ptr; +} + +bool DoHook(ArtMethod *target, ArtMethod *hook, ArtMethod *backup) { + ScopedGCCriticalSection section(art::Thread::Current(), + art::gc::kGcCauseDebugger, + art::gc::kCollectorTypeDebugger); + ScopedSuspendAll suspend("Yahfa Hook", false); + LOGD("target = %p, hook = %p, backup = %p", target, hook, backup); + + if (auto *trampoline = GenerateTrampolineFor(hook); !trampoline) { + LOGE("Failed to generate trampoline"); + return false; + // NOLINTNEXTLINE + } else { + LOGV("Generated trampoline %p", trampoline); + + target->SetNonCompilable(); + hook->SetNonCompilable(); + + // copy after setNonCompilable + backup->CopyFrom(target); + + target->SetNonIntrinsic(); + + target->SetEntryPoint(trampoline); + + backup->SetPrivate(); + + LOGD("done hook"); + LOGV("target(%p:0x%x) -> %p; backup(%p:0x%x) -> %p; hook(%p:0x%x) -> %p", + target, target->GetAccessFlags(), target->GetEntryPoint(), + backup, backup->GetAccessFlags(), backup->GetEntryPoint(), + hook, hook->GetAccessFlags(), hook->GetEntryPoint()); + + RecordHooked(target, backup); + return true; + } +} + +bool DoUnHook(ArtMethod *target, ArtMethod *backup) { + ScopedGCCriticalSection section(art::Thread::Current(), + art::gc::kGcCauseDebugger, + art::gc::kCollectorTypeDebugger); + ScopedSuspendAll suspend("Yahfa Hook", false); + auto access_flags = target->GetAccessFlags(); + target->CopyFrom(backup); + target->SetAccessFlags(access_flags); + return true; +} + +} // namespace + +void OnPending(art::ArtMethod *target, art::ArtMethod *hook, art::ArtMethod *backup) { + LOGD("On pending hook"); + if (!DoHook(target, hook, backup)) { + LOGE("Pending hook failed"); + } +} + +[[maybe_unused]] +bool Init(JNIEnv *env, const InitInfo &info) { + bool static kInit = InitJNI(env) && InitNative(env, info); + return kInit; +} + + +// TODO: sync? does not record hook immediately +[[maybe_unused]] +jmethodID +Hook(JNIEnv *env, jmethodID target_method, jobject hooker_object, jmethodID callback_method) { + auto reflected_target = JNI_ToReflectedMethod(env, jclass{ nullptr }, target_method, false); + auto reflected_callback = JNI_ToReflectedMethod(env, jclass{ nullptr }, callback_method, false); + jmethodID hook_method = nullptr; + jmethodID backup_method = nullptr; + jfieldID hooker_field = nullptr; + + auto target_class = JNI_Cast( + JNI_CallObjectMethod(env, reflected_target, method_get_declaring_class)); + bool is_proxy = JNI_CallBooleanMethod(env, target_class, class_is_proxy); + auto *target = ArtMethod::FromReflectedMethod(env, reflected_target); + bool is_static = target->IsStatic(); + + if (IsHooked(target) || IsPending(target)) { + LOGW("Skip duplicate hook"); + return nullptr; + } + + ScopedLocalRef built_class{ env }; + { + auto callback_name = JNI_Cast( + JNI_CallObjectMethod(env, reflected_callback, method_get_name)); + JUTFString method_name(callback_name); + auto callback_class = JNI_Cast(JNI_CallObjectMethod(env, reflected_callback, + method_get_declaring_class)); + auto callback_class_loader = JNI_CallObjectMethod(env, callback_class, + class_get_class_loader); + auto callback_class_name = JNI_Cast(JNI_CallObjectMethod(env, callback_class, + class_get_canonical_name)); + JUTFString class_name(callback_class_name); + if (env->IsInstanceOf(hooker_object, callback_class)) { + LOGE("callback_method is not a method of hooker_object"); + return nullptr; + } + std::tie(built_class, hooker_field, hook_method, backup_method) = + WrapScope(env, BuildDex(env, callback_class_loader, + ArtMethod::GetMethodShorty(env, target_method), + is_static, + method_name.get(), + class_name.get(), + method_name.get())); + if (!built_class || !hooker_field || !hook_method || !backup_method) { + LOGE("Failed to generate hooker"); + return nullptr; + } + } + + auto reflected_hook = JNI_ToReflectedMethod(env, jclass{ nullptr }, hook_method, false); + auto reflected_backup = JNI_ToReflectedMethod(env, jclass{ nullptr }, backup_method, false); + + auto *hook = ArtMethod::FromReflectedMethod(env, reflected_hook); + auto *backup = ArtMethod::FromReflectedMethod(env, reflected_backup); + + env->SetStaticObjectField(built_class.get(), hooker_field, hooker_object); + + if (is_static && !Class::IsInitialized(env, target_class.get())) { + auto *miror_class = Class::FromReflectedClass(env, target_class); + if (!miror_class) { + LOGE("Failed to decode target class"); + return nullptr; + } + auto class_def = miror_class->GetClassDef(); + if (!class_def) { + LOGE("Failed to get target class def"); + return nullptr; + } + RecordPending(class_def, target, hook, backup); + return backup_method; + } else if (DoHook(target, hook, backup)) { + JNI_NewGlobalRef(env, reflected_hook); + JNI_NewGlobalRef(env, reflected_backup); + if (!is_proxy) [[likely]] RecordJitMovement(target, backup); + return backup_method; + } + + return nullptr; +} + +[[maybe_unused]] +bool UnHook(JNIEnv *env, jmethodID target_method) { + auto reflected_target = JNI_ToReflectedMethod(env, jclass{ nullptr }, target_method, false); + auto *target = ArtMethod::FromReflectedMethod(env, reflected_target); + ArtMethod *backup = nullptr; + { + std::unique_lock lk(pending_methods_lock_); + if (auto it = pending_methods_.find(target); it != pending_methods_.end()) { + pending_methods_.erase(it); + return true; + } + } + { + std::unique_lock lk(hooked_methods_lock_); + if (auto it = hooked_methods_.find(target);it != hooked_methods_.end()) { + backup = it->second; + hooked_methods_.erase(it); + } + } + if (backup == nullptr) { + LOGE("Unable to unhook a method that is not hooked"); + return false; + } + return DoUnHook(target, backup); +} + +[[maybe_unused]] +bool IsHooked(JNIEnv *env, jmethodID method) { + auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); + auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + + if (std::shared_lock lk(hooked_methods_lock_); hooked_methods_.contains(art_method)) { + return true; + } + if (std::shared_lock lk(pending_methods_lock_); pending_methods_.contains(art_method)) { + return true; + } + return false; +} + +[[maybe_unused]] +bool Deoptimize(JNIEnv *env, jmethodID method) { + auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); + auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + return ClassLinker::SetEntryPointsToInterpreter(art_method); +} + +[[maybe_unused]] +void *GetNativeFunction(JNIEnv *env, jmethodID method) { + auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); + auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + return art_method->GetData(); +} + +} // namespace lsplant + +#pragma clang diagnostic pop diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..64c2a1c --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3839100 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,12 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "LSPlant" +include( + ":library", +)