├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── module ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ ├── CMakeLists.txt │ ├── logging.h │ ├── main.cpp │ ├── misc.cpp │ ├── misc.h │ └── zygisk.hpp │ └── res │ └── values │ └── strings.xml ├── settings.gradle └── template └── magisk_module ├── .gitattributes ├── META-INF └── com │ └── google │ └── android │ ├── update-binary │ └── updater-script ├── README.md ├── customize.sh ├── module.prop ├── post-fs-data.sh ├── util_functions.sh └── verify.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.jar binary -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: [ v* ] 7 | pull_request: 8 | merge_group: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: "recursive" 19 | fetch-depth: 0 20 | 21 | - name: Setup Java 22 | uses: actions/setup-java@v4 23 | with: 24 | distribution: "temurin" 25 | java-version: "21" 26 | 27 | - name: Setup ninja 28 | uses: seanmiddleditch/gha-setup-ninja@master 29 | with: 30 | version: 1.12.0 31 | 32 | - name: Build with Gradle 33 | run: | 34 | ./gradlew build 35 | 36 | - name: Upload release 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: FontLoader-release 40 | path: "out/release/*" 41 | 42 | - name: Upload debug 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: FontLoader-debug 46 | path: "out/debug/*" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /out 17 | /.idea 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FontLoader 2 | 3 | Modifying fonts is a common scenario using the Magisk module. For example, fonts for CJK languages in the Android system only have one font-weight, users can use the Magisk module to modify font files to add the remaining font weights. 4 | 5 | However, starting from Android 12, fonts are loaded only when the app needs to render the font. Before, fonts are preloaded in the zygote process. When users revert the change of Magisk (with MagiskHide before or DenyList nowadays), apps will not be able to access font files from modules and finally result in a crash. 6 | 7 | This module is a Zygisk module that is designed to solve the problem. The principle is simple, preload the font when the app has not yet lost access to the font. 8 | 9 | ## Usage 10 | 11 | 1. Install FontLoader module in Magisk app 12 | 2. Remove target apps with font customizations out of DenyList 13 | 14 | To be clear, DenyList is NOT for hiding purposes. This is as topjohnwu, the author of Magisk, said. And using DenyList for hiding is not enough. 15 | 16 | ## Something else 17 | 18 | * Why not using the font upgrade feature from Android 12? 19 | 20 | The font upgrade feature cannot set the default font-weight and language. Therefore it can only be used to upgrade/add a font with a single font-weight like emoji font. This is exactly how it is used in the documentation. 21 | 22 | Also, the font upgrade feature requires signing font files. It is impossible to add our key without modifying Magisk. 23 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | apply plugin: 'idea' 3 | 4 | idea.module { 5 | excludeDirs += file('out') 6 | resourceDirs += file('template') 7 | } 8 | 9 | buildscript { 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath 'com.android.tools.build:gradle:8.7.3' 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 24 | } 25 | 26 | ext { 27 | moduleId = 'font-loader' 28 | moduleName = 'Font Loader' 29 | moduleDescription = 'Preload font modules for Android 12. Do not enable useless DenyList and use hide module for hiding ROOT instead!' 30 | moduleAuthor = 'JingMatrix, Rikka' 31 | moduleVersionName = '1.0.1' 32 | moduleVersionCode = 2 33 | } 34 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | #org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JingMatrix/FontLoader/6a9cb72f72a4751cd3e2d34548e0ccf65e3a123f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /module/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /module/build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.FixCrLfFilter 2 | 3 | import java.security.MessageDigest 4 | 5 | plugins { 6 | id 'com.android.application' 7 | } 8 | 9 | android { 10 | compileSdk 35 11 | namespace "rikka.fontloader" 12 | defaultConfig { 13 | applicationId "rikka.fontloader" 14 | minSdk 31 15 | targetSdk 35 16 | versionCode 1 17 | versionName "1.0" 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | externalNativeBuild { 20 | cmake { 21 | arguments '-DANDROID_STL=none' 22 | } 23 | } 24 | } 25 | buildFeatures { 26 | buildConfig false 27 | prefab true 28 | } 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | compileOptions { 36 | sourceCompatibility JavaVersion.VERSION_21 37 | targetCompatibility JavaVersion.VERSION_21 38 | } 39 | externalNativeBuild { 40 | cmake { 41 | path file('src/main/cpp/CMakeLists.txt') 42 | version '3.19.0+' 43 | buildStagingDirectory layout.buildDirectory 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation 'dev.rikka.ndk.thirdparty:cxx:1.2.0' 50 | implementation 'dev.rikka.ndk.thirdparty:nativehelper:1.0.1' 51 | implementation 'dev.rikka.ndk.thirdparty:proc-maps-parser:1.0.0' 52 | } 53 | 54 | afterEvaluate { 55 | def isIDE = properties.containsKey('android.injected.invoked.from.ide') 56 | if (isIDE) { 57 | println("Invoked from IDE") 58 | } else { 59 | println("Invoked from command line") 60 | } 61 | 62 | android.applicationVariants.all { variant -> 63 | def outDir = file("$rootDir/out") 64 | def variantCapped = variant.name.capitalize() 65 | def variantLowered = variant.name.toLowerCase() 66 | def buildTypeCapped = variant.getBuildType().getName().capitalize() 67 | def buildTypeLowered = variant.getBuildType().getName().toLowerCase() 68 | 69 | def zipName = "font-loader-${moduleVersionName}-${buildTypeLowered}.zip" 70 | def magiskDir = file("$outDir/${buildTypeLowered}") 71 | 72 | task("prepareMagiskFiles${variantCapped}", type: Sync) { 73 | dependsOn("assemble$variantCapped") 74 | 75 | def templatePath = "$rootDir/template/magisk_module" 76 | 77 | into magiskDir 78 | from(templatePath) { 79 | exclude 'module.prop' 80 | } 81 | from(templatePath) { 82 | include 'module.prop' 83 | expand([ 84 | id : moduleId, 85 | name : moduleName, 86 | version : moduleVersionName, 87 | versionCode: moduleVersionCode.toString(), 88 | author : moduleAuthor, 89 | description: moduleDescription, 90 | ]) 91 | filter(FixCrLfFilter.class, 92 | eol: FixCrLfFilter.CrLf.newInstance("lf")) 93 | } 94 | /*from((buildTypeLowered == "release") ? 95 | "$buildDir/intermediates/dex/${variant.name}/minify${variantCapped}WithR8" : 96 | "$buildDir/intermediates/dex/${variant.name}/mergeDex$variantCapped") { 97 | include 'classes.dex' 98 | rename { 'sui.dex' } 99 | }*/ 100 | from("$buildDir/intermediates/stripped_native_libs/${variantLowered}/strip${variantCapped}DebugSymbols/out/lib") { 101 | into 'lib' 102 | } 103 | doLast { 104 | fileTree("$magiskDir").visit { f -> 105 | if (f.directory) return 106 | if (f.file.name == '.gitattributes') return 107 | 108 | def md = MessageDigest.getInstance("SHA-256") 109 | f.file.eachByte 4096, { bytes, size -> 110 | md.update(bytes, 0, size) 111 | } 112 | file(f.file.path + ".sha256sum").text = md.digest().encodeHex() 113 | } 114 | } 115 | } 116 | 117 | task("zip${variantCapped}", type: Zip) { 118 | dependsOn("prepareMagiskFiles${variantCapped}") 119 | from magiskDir 120 | archiveFileName = zipName 121 | destinationDirectory = outDir 122 | } 123 | 124 | task("push${variantCapped}", type: Exec) { 125 | dependsOn("assemble${variantCapped}") 126 | workingDir outDir 127 | commandLine android.adbExecutable, "push", zipName, "/data/local/tmp/" 128 | } 129 | 130 | task("flash${variantCapped}", type: Exec) { 131 | dependsOn("push${variantCapped}") 132 | commandLine android.adbExecutable, "shell", "su", "-c", 133 | "magisk --install-module /data/local/tmp/${zipName}" 134 | } 135 | 136 | task("flashAndReboot${variantCapped}", type: Exec) { 137 | dependsOn("flash${variantCapped}") 138 | commandLine android.adbExecutable, "shell", "reboot" 139 | } 140 | 141 | variant.assembleProvider.get().finalizedBy("zip${variantCapped}") 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /module/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /module/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /module/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.19.0) 2 | 3 | project("fontloader") 4 | 5 | set(CMAKE_CXX_STANDARD 20) 6 | 7 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 8 | 9 | set(LINKER_FLAGS "-ffixed-x18 -Wl,--hash-style=both") 10 | set(C_FLAGS "-Werror=format -fdata-sections -ffunction-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics") 11 | 12 | if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") 13 | set(C_FLAGS "${C_FLAGS} -O2 -fvisibility=hidden -fvisibility-inlines-hidden") 14 | set(LINKER_FLAGS "${LINKER_FLAGS} -Wl,-exclude-libs,ALL -Wl,--gc-sections") 15 | else () 16 | add_definitions(-DDEBUG) 17 | set(C_FLAGS "${C_FLAGS} -O0") 18 | endif () 19 | 20 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_FLAGS}") 21 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${C_FLAGS}") 22 | 23 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") 24 | set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}") 25 | 26 | find_package(cxx REQUIRED CONFIG) 27 | find_package(nativehelper REQUIRED CONFIG) 28 | find_package(proc-maps-parser REQUIRED CONFIG) 29 | 30 | add_library(fontloader SHARED main.cpp misc.cpp) 31 | 32 | target_link_libraries(fontloader 33 | log 34 | cxx::cxx 35 | nativehelper::nativehelper_header_only 36 | proc-maps-parser::proc-maps-parser) 37 | -------------------------------------------------------------------------------- /module/src/main/cpp/logging.h: -------------------------------------------------------------------------------- 1 | #ifndef _LOGGING_H 2 | #define _LOGGING_H 3 | 4 | #include 5 | #include "android/log.h" 6 | 7 | #ifndef LOG_TAG 8 | #define LOG_TAG "FontLoader" 9 | #endif 10 | 11 | #ifndef NO_LOG 12 | #ifndef NO_DEBUG_LOG 13 | #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) 14 | #else 15 | #define LOGD(...) 16 | #endif 17 | #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) 18 | #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) 19 | #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) 20 | #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) 21 | #define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) 22 | #else 23 | #define LOGD(...) 24 | #define LOGV(...) 25 | #define LOGI(...) 26 | #define LOGW(...) 27 | #define LOGE(...) 28 | #define PLOGE(fmt, args...) 29 | #endif 30 | #endif // _LOGGING_H 31 | -------------------------------------------------------------------------------- /module/src/main/cpp/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "logging.h" 15 | #include "zygisk.hpp" 16 | #include "misc.h" 17 | 18 | using zygisk::Api; 19 | using zygisk::AppSpecializeArgs; 20 | using zygisk::ServerSpecializeArgs; 21 | 22 | static int GetProt(const procmaps_struct *procstruct) { 23 | int prot = 0; 24 | if (procstruct->is_r) { 25 | prot |= PROT_READ; 26 | } 27 | if (procstruct->is_w) { 28 | prot |= PROT_WRITE; 29 | } 30 | if (procstruct->is_x) { 31 | prot |= PROT_EXEC; 32 | } 33 | return prot; 34 | } 35 | 36 | static void HideFromMaps(const std::vector &fonts) { 37 | std::unique_ptr maps{pmparser_parse(-1), &pmparser_free}; 38 | if (!maps) { 39 | LOGW("failed to parse /proc/self/maps"); 40 | return; 41 | } 42 | 43 | for (procmaps_struct *i = pmparser_next(maps.get()); i; i = pmparser_next(maps.get())) { 44 | using namespace std::string_view_literals; 45 | std::string_view pathname = i->pathname; 46 | 47 | if (std::find(fonts.begin(), fonts.end(), pathname) == fonts.end()) continue; 48 | 49 | auto start = reinterpret_cast(i->addr_start); 50 | auto end = reinterpret_cast(i->addr_end); 51 | if (end <= start) continue; 52 | auto len = end - start; 53 | auto *bk = mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 54 | -1, 0); 55 | if (bk == MAP_FAILED) continue; 56 | auto old_prot = GetProt(i); 57 | if (!i->is_r && mprotect(i->addr_start, len, old_prot | PROT_READ) != 0) { 58 | PLOGE("Failed to hide %*s from maps", static_cast(pathname.size()), 59 | pathname.data()); 60 | continue; 61 | } 62 | memcpy(bk, i->addr_start, len); 63 | mremap(bk, len, len, MREMAP_FIXED | MREMAP_MAYMOVE, i->addr_start); 64 | mprotect(i->addr_start, len, old_prot); 65 | 66 | LOGV("Hide %*s from maps", static_cast(pathname.size()), pathname.data()); 67 | } 68 | } 69 | 70 | static void PreloadFonts(JNIEnv *env, const std::vector &fonts) { 71 | auto typefaceClass = env->FindClass("android/graphics/Typeface"); 72 | auto methodId = env->GetStaticMethodID(typefaceClass, "nativeWarmUpCache", "(Ljava/lang/String;)V"); 73 | if (!methodId) { 74 | env->ExceptionClear(); 75 | return; 76 | } 77 | 78 | for (const std::string &font : fonts) { 79 | env->CallStaticVoidMethod(typefaceClass, methodId, env->NewStringUTF(font.c_str())); 80 | if (env->ExceptionCheck()) { 81 | LOGW("Preload font %s failed", font.c_str()); 82 | env->ExceptionDescribe(); 83 | env->ExceptionClear(); 84 | } else { 85 | LOGV("Preloaded font %s", font.c_str()); 86 | } 87 | } 88 | } 89 | 90 | class ZygiskModule : public zygisk::ModuleBase { 91 | public: 92 | void onLoad(Api *_api, JNIEnv *_env) override { 93 | this->api = _api; 94 | this->env = _env; 95 | } 96 | 97 | void preAppSpecialize(AppSpecializeArgs *args) override { 98 | InitCompanion(); 99 | PreloadFonts(env, fonts); 100 | 101 | api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY); 102 | } 103 | 104 | void preServerSpecialize(ServerSpecializeArgs *args) override { 105 | api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY); 106 | } 107 | 108 | private: 109 | Api *api{}; 110 | JNIEnv *env{}; 111 | std::vector fonts; 112 | 113 | void InitCompanion() { 114 | auto companion = api->connectCompanion(); 115 | if (companion == -1) { 116 | LOGE("Failed to connect to companion"); 117 | return; 118 | } 119 | 120 | char path[PATH_MAX]{}; 121 | auto size = read_int(companion); 122 | for (int i = 0; i < size; ++i) { 123 | auto string_size = read_int(companion); 124 | read_full(companion, path, string_size); 125 | path[string_size] = '\0'; 126 | fonts.emplace_back(path); 127 | } 128 | 129 | close(companion); 130 | } 131 | }; 132 | 133 | static bool PrepareCompanion(std::vector &fonts) { 134 | bool result = false; 135 | char path[PATH_MAX]{}; 136 | struct dirent *entry; 137 | 138 | ScopedReaddir modules("/data/adb/modules/"); 139 | if (modules.IsBad()) goto clean; 140 | 141 | while ((entry = modules.ReadEntry())) { 142 | if (entry->d_type != DT_DIR) continue; 143 | if (entry->d_name[0] == '.') continue; 144 | 145 | snprintf(path, PATH_MAX, "/data/adb/modules/%s/disable", entry->d_name); 146 | if (access(path, F_OK) == 0) { 147 | LOGV("Module %s is disabled", entry->d_name); 148 | continue; 149 | } 150 | 151 | snprintf(path, PATH_MAX, "/data/adb/modules/%s/system/fonts", entry->d_name); 152 | if (access(path, F_OK) != 0) { 153 | LOGV("Module %s does not contain font", entry->d_name); 154 | continue; 155 | } 156 | 157 | ScopedReaddir dir(path); 158 | if (dir.IsBad()) { 159 | LOGW("Cannot open %s", path); 160 | continue; 161 | } 162 | 163 | while ((entry = dir.ReadEntry())) { 164 | if (entry->d_type != DT_REG) continue; 165 | if (entry->d_name[0] == '.') continue; 166 | 167 | snprintf(path, PATH_MAX, "/system/fonts/%s", entry->d_name); 168 | if (access(path, F_OK) == 0) { 169 | fonts.emplace_back(path); 170 | LOGI("Collected font %s", path); 171 | } else { 172 | LOGW("Font %s does not exist", path); 173 | } 174 | } 175 | } 176 | 177 | result = true; 178 | 179 | clean: 180 | return result; 181 | } 182 | 183 | static void CompanionEntry(int socket) { 184 | static std::vector fonts; 185 | static auto prepare = PrepareCompanion(fonts); 186 | 187 | write_int(socket, fonts.size()); 188 | 189 | for (const std::string &font: fonts) { 190 | auto size = font.size(); 191 | auto array = font.c_str(); 192 | write_int(socket, size); 193 | write_full(socket, array, size); 194 | } 195 | 196 | close(socket); 197 | } 198 | 199 | REGISTER_ZYGISK_MODULE(ZygiskModule) 200 | 201 | REGISTER_ZYGISK_COMPANION(CompanionEntry) 202 | -------------------------------------------------------------------------------- /module/src/main/cpp/misc.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | ssize_t read_eintr(int fd, void *out, size_t len) { 6 | ssize_t ret; 7 | do { 8 | ret = read(fd, out, len); 9 | } while (ret < 0 && errno == EINTR); 10 | return ret; 11 | } 12 | 13 | int read_full(int fd, void *out, size_t len) { 14 | while (len > 0) { 15 | ssize_t ret = read_eintr(fd, out, len); 16 | if (ret <= 0) { 17 | return -1; 18 | } 19 | out = (void *) ((uintptr_t) out + ret); 20 | len -= ret; 21 | } 22 | return 0; 23 | } 24 | 25 | int write_full(int fd, const void *buf, size_t count) { 26 | while (count > 0) { 27 | ssize_t size = write(fd, buf, count < SSIZE_MAX ? count : SSIZE_MAX); 28 | if (size == -1) { 29 | if (errno == EINTR) 30 | continue; 31 | else 32 | return -1; 33 | } 34 | 35 | buf = (const void *) ((uintptr_t) buf + size); 36 | count -= size; 37 | } 38 | return 0; 39 | } 40 | 41 | int read_int(int fd) { 42 | int val; 43 | if (read_full(fd, &val, sizeof(val)) != 0) 44 | return -1; 45 | return val; 46 | } 47 | 48 | void write_int(int fd, int val) { 49 | if (fd < 0) return; 50 | write_full(fd, &val, sizeof(val)); 51 | } 52 | -------------------------------------------------------------------------------- /module/src/main/cpp/misc.h: -------------------------------------------------------------------------------- 1 | #ifndef MISC_H 2 | #define MISC_H 3 | 4 | int read_full(int fd, void *buf, size_t count); 5 | 6 | int write_full(int fd, const void *buf, size_t count); 7 | 8 | int read_int(int fd); 9 | 10 | void write_int(int fd, int val); 11 | 12 | #endif // MISC_H 13 | -------------------------------------------------------------------------------- /module/src/main/cpp/zygisk.hpp: -------------------------------------------------------------------------------- 1 | /* Copyright 2022-2023 John "topjohnwu" Wu 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted. 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 8 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | * PERFORMANCE OF THIS SOFTWARE. 13 | */ 14 | 15 | // This is the public API for Zygisk modules. 16 | // DO NOT MODIFY ANY CODE IN THIS HEADER. 17 | 18 | // WARNING: this file may contain changes that are not finalized. 19 | // Always use the following published header for development: 20 | // https://github.com/topjohnwu/zygisk-module-sample/blob/master/module/jni/zygisk.hpp 21 | 22 | #pragma once 23 | 24 | #include 25 | 26 | #define ZYGISK_API_VERSION 4 27 | 28 | /* 29 | 30 | *************** 31 | * Introduction 32 | *************** 33 | 34 | On Android, all app processes are forked from a special daemon called "Zygote". 35 | For each new app process, zygote will fork a new process and perform "specialization". 36 | This specialization operation enforces the Android security sandbox on the newly forked 37 | process to make sure that 3rd party application code is only loaded after it is being 38 | restricted within a sandbox. 39 | 40 | On Android, there is also this special process called "system_server". This single 41 | process hosts a significant portion of system services, which controls how the 42 | Android operating system and apps interact with each other. 43 | 44 | The Zygisk framework provides a way to allow developers to build modules and run custom 45 | code before and after system_server and any app processes' specialization. 46 | This enable developers to inject code and alter the behavior of system_server and app processes. 47 | 48 | Please note that modules will only be loaded after zygote has forked the child process. 49 | THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON! 50 | 51 | ********************* 52 | * Development Guide 53 | ********************* 54 | 55 | Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. 56 | Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. 57 | 58 | Example code: 59 | 60 | static jint (*orig_logger_entry_max)(JNIEnv *env); 61 | static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } 62 | 63 | class ExampleModule : public zygisk::ModuleBase { 64 | public: 65 | void onLoad(zygisk::Api *api, JNIEnv *env) override { 66 | this->api = api; 67 | this->env = env; 68 | } 69 | void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { 70 | JNINativeMethod methods[] = { 71 | { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, 72 | }; 73 | api->hookJniNativeMethods(env, "android/util/Log", methods, 1); 74 | *(void **) &orig_logger_entry_max = methods[0].fnPtr; 75 | } 76 | private: 77 | zygisk::Api *api; 78 | JNIEnv *env; 79 | }; 80 | 81 | REGISTER_ZYGISK_MODULE(ExampleModule) 82 | 83 | ----------------------------------------------------------------------------------------- 84 | 85 | Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize, 86 | or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class 87 | never runs in a true superuser environment. 88 | 89 | If your module require access to superuser permissions, you can create and register 90 | a root companion handler function. This function runs in a separate root companion 91 | daemon process, and an Unix domain socket is provided to allow you to perform IPC between 92 | your target process and the root companion process. 93 | 94 | Example code: 95 | 96 | static void example_handler(int socket) { ... } 97 | 98 | REGISTER_ZYGISK_COMPANION(example_handler) 99 | 100 | */ 101 | 102 | namespace zygisk { 103 | 104 | struct Api; 105 | struct AppSpecializeArgs; 106 | struct ServerSpecializeArgs; 107 | 108 | class ModuleBase { 109 | public: 110 | 111 | // This method is called as soon as the module is loaded into the target process. 112 | // A Zygisk API handle will be passed as an argument. 113 | virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} 114 | 115 | // This method is called before the app process is specialized. 116 | // At this point, the process just got forked from zygote, but no app specific specialization 117 | // is applied. This means that the process does not have any sandbox restrictions and 118 | // still runs with the same privilege of zygote. 119 | // 120 | // All the arguments that will be sent and used for app specialization is passed as a single 121 | // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app 122 | // process will be specialized. 123 | // 124 | // If you need to run some operations as superuser, you can call Api::connectCompanion() to 125 | // get a socket to do IPC calls with a root companion process. 126 | // See Api::connectCompanion() for more info. 127 | virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} 128 | 129 | // This method is called after the app process is specialized. 130 | // At this point, the process has all sandbox restrictions enabled for this application. 131 | // This means that this method runs with the same privilege of the app's own code. 132 | virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} 133 | 134 | // This method is called before the system server process is specialized. 135 | // See preAppSpecialize(args) for more info. 136 | virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} 137 | 138 | // This method is called after the system server process is specialized. 139 | // At this point, the process runs with the privilege of system_server. 140 | virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} 141 | }; 142 | 143 | struct AppSpecializeArgs { 144 | // Required arguments. These arguments are guaranteed to exist on all Android versions. 145 | jint &uid; 146 | jint &gid; 147 | jintArray &gids; 148 | jint &runtime_flags; 149 | jobjectArray &rlimits; 150 | jint &mount_external; 151 | jstring &se_info; 152 | jstring &nice_name; 153 | jstring &instruction_set; 154 | jstring &app_data_dir; 155 | 156 | // Optional arguments. Please check whether the pointer is null before de-referencing 157 | jintArray *const fds_to_ignore; 158 | jboolean *const is_child_zygote; 159 | jboolean *const is_top_app; 160 | jobjectArray *const pkg_data_info_list; 161 | jobjectArray *const whitelisted_data_info_list; 162 | jboolean *const mount_data_dirs; 163 | jboolean *const mount_storage_dirs; 164 | jboolean *const mount_sysprop_overrides; 165 | 166 | AppSpecializeArgs() = delete; 167 | }; 168 | 169 | struct ServerSpecializeArgs { 170 | jint &uid; 171 | jint &gid; 172 | jintArray &gids; 173 | jint &runtime_flags; 174 | jlong &permitted_capabilities; 175 | jlong &effective_capabilities; 176 | 177 | ServerSpecializeArgs() = delete; 178 | }; 179 | 180 | namespace internal { 181 | struct api_table; 182 | template void entry_impl(api_table *, JNIEnv *); 183 | } 184 | 185 | // These values are used in Api::setOption(Option) 186 | enum Option : int { 187 | // Force Magisk's denylist unmount routines to run on this process. 188 | // 189 | // Setting this option only makes sense in preAppSpecialize. 190 | // The actual unmounting happens during app process specialization. 191 | // 192 | // Set this option to force all Magisk and modules' files to be unmounted from the 193 | // mount namespace of the process, regardless of the denylist enforcement status. 194 | FORCE_DENYLIST_UNMOUNT = 0, 195 | 196 | // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. 197 | // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. 198 | // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. 199 | DLCLOSE_MODULE_LIBRARY = 1, 200 | }; 201 | 202 | // Bit masks of the return value of Api::getFlags() 203 | enum StateFlag : uint32_t { 204 | // The user has granted root access to the current process 205 | PROCESS_GRANTED_ROOT = (1u << 0), 206 | 207 | // The current process was added on the denylist 208 | PROCESS_ON_DENYLIST = (1u << 1), 209 | }; 210 | 211 | // All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded 212 | // from the specialized process afterwards. 213 | struct Api { 214 | 215 | // Connect to a root companion process and get a Unix domain socket for IPC. 216 | // 217 | // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. 218 | // 219 | // The pre[XXX]Specialize methods run with the same privilege of zygote. 220 | // If you would like to do some operations with superuser permissions, register a handler 221 | // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). 222 | // Another good use case for a companion process is that if you want to share some resources 223 | // across multiple processes, hold the resources in the companion process and pass it over. 224 | // 225 | // The root companion process is ABI aware; that is, when calling this method from a 32-bit 226 | // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. 227 | // 228 | // Returns a file descriptor to a socket that is connected to the socket passed to your 229 | // module's companion request handler. Returns -1 if the connection attempt failed. 230 | int connectCompanion(); 231 | 232 | // Get the file descriptor of the root folder of the current module. 233 | // 234 | // This API only works in the pre[XXX]Specialize methods. 235 | // Accessing the directory returned is only possible in the pre[XXX]Specialize methods 236 | // or in the root companion process (assuming that you sent the fd over the socket). 237 | // Both restrictions are due to SELinux and UID. 238 | // 239 | // Returns -1 if errors occurred. 240 | int getModuleDir(); 241 | 242 | // Set various options for your module. 243 | // Please note that this method accepts one single option at a time. 244 | // Check zygisk::Option for the full list of options available. 245 | void setOption(Option opt); 246 | 247 | // Get information about the current process. 248 | // Returns bitwise-or'd zygisk::StateFlag values. 249 | uint32_t getFlags(); 250 | 251 | // Exempt the provided file descriptor from being automatically closed. 252 | // 253 | // This API only make sense in preAppSpecialize; calling this method in any other situation 254 | // is either a no-op (returns true) or an error (returns false). 255 | // 256 | // When false is returned, the provided file descriptor will eventually be closed by zygote. 257 | bool exemptFd(int fd); 258 | 259 | // Hook JNI native methods for a class 260 | // 261 | // Lookup all registered JNI native methods and replace it with your own methods. 262 | // The original function pointer will be saved in each JNINativeMethod's fnPtr. 263 | // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr 264 | // will be set to nullptr. 265 | void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); 266 | 267 | // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. 268 | // 269 | // Parsing /proc/[PID]/maps will give you the memory map of a process. As an example: 270 | // 271 | //
272 | // 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64 273 | // (More details: https://man7.org/linux/man-pages/man5/proc.5.html) 274 | // 275 | // The `dev` and `inode` pair uniquely identifies a file being mapped into memory. 276 | // For matching ELFs loaded in memory, replace function `symbol` with `newFunc`. 277 | // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. 278 | void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc); 279 | 280 | // Commit all the hooks that was previously registered. 281 | // Returns false if an error occurred. 282 | bool pltHookCommit(); 283 | 284 | private: 285 | internal::api_table *tbl; 286 | template friend void internal::entry_impl(internal::api_table *, JNIEnv *); 287 | }; 288 | 289 | // Register a class as a Zygisk module 290 | 291 | #define REGISTER_ZYGISK_MODULE(clazz) \ 292 | void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ 293 | zygisk::internal::entry_impl(table, env); \ 294 | } 295 | 296 | // Register a root companion request handler function for your module 297 | // 298 | // The function runs in a superuser daemon process and handles a root companion request from 299 | // your module running in a target process. The function has to accept an integer value, 300 | // which is a Unix domain socket that is connected to the target process. 301 | // See Api::connectCompanion() for more info. 302 | // 303 | // NOTE: the function can run concurrently on multiple threads. 304 | // Be aware of race conditions if you have globally shared resources. 305 | 306 | #define REGISTER_ZYGISK_COMPANION(func) \ 307 | void zygisk_companion_entry(int client) { func(client); } 308 | 309 | /********************************************************* 310 | * The following is internal ABI implementation detail. 311 | * You do not have to understand what it is doing. 312 | *********************************************************/ 313 | 314 | namespace internal { 315 | 316 | struct module_abi { 317 | long api_version; 318 | ModuleBase *impl; 319 | 320 | void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); 321 | void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); 322 | void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); 323 | void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); 324 | 325 | module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) { 326 | preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); }; 327 | postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); }; 328 | preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); }; 329 | postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); }; 330 | } 331 | }; 332 | 333 | struct api_table { 334 | // Base 335 | void *impl; 336 | bool (*registerModule)(api_table *, module_abi *); 337 | 338 | void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); 339 | void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **); 340 | bool (*exemptFd)(int); 341 | bool (*pltHookCommit)(); 342 | int (*connectCompanion)(void * /* impl */); 343 | void (*setOption)(void * /* impl */, Option); 344 | int (*getModuleDir)(void * /* impl */); 345 | uint32_t (*getFlags)(void * /* impl */); 346 | }; 347 | 348 | template 349 | void entry_impl(api_table *table, JNIEnv *env) { 350 | static Api api; 351 | api.tbl = table; 352 | static T module; 353 | ModuleBase *m = &module; 354 | static module_abi abi(m); 355 | if (!table->registerModule(table, &abi)) return; 356 | m->onLoad(&api, env); 357 | } 358 | 359 | } // namespace internal 360 | 361 | inline int Api::connectCompanion() { 362 | return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; 363 | } 364 | inline int Api::getModuleDir() { 365 | return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; 366 | } 367 | inline void Api::setOption(Option opt) { 368 | if (tbl->setOption) tbl->setOption(tbl->impl, opt); 369 | } 370 | inline uint32_t Api::getFlags() { 371 | return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; 372 | } 373 | inline bool Api::exemptFd(int fd) { 374 | return tbl->exemptFd != nullptr && tbl->exemptFd(fd); 375 | } 376 | inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { 377 | if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods); 378 | } 379 | inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) { 380 | if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc); 381 | } 382 | inline bool Api::pltHookCommit() { 383 | return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); 384 | } 385 | 386 | } // namespace zygisk 387 | 388 | extern "C" { 389 | 390 | [[gnu::visibility("default"), maybe_unused]] 391 | void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); 392 | 393 | [[gnu::visibility("default"), maybe_unused]] 394 | void zygisk_companion_entry(int); 395 | 396 | } // extern "C" 397 | -------------------------------------------------------------------------------- /module/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FontLoader 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | rootProject.name = "FontLoader" 9 | include ':module' 10 | -------------------------------------------------------------------------------- /template/magisk_module/.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files that will always have LF line endings on checkout. 2 | META-INF/** text eol=lf 3 | *.prop text eol=lf 4 | *.sh text eol=lf 5 | *.md text eol=lf 6 | sepolicy.rule text eol=lf 7 | 8 | # Denote all files that are truly binary and should not be modified. 9 | lib/** binary -------------------------------------------------------------------------------- /template/magisk_module/META-INF/com/google/android/update-binary: -------------------------------------------------------------------------------- 1 | #!/sbin/sh 2 | 3 | ################# 4 | # Initialization 5 | ################# 6 | 7 | umask 022 8 | 9 | # echo before loading util_functions 10 | ui_print() { echo "$1"; } 11 | 12 | require_new_magisk() { 13 | ui_print "*******************************" 14 | ui_print " Please install Magisk v20.4+! " 15 | ui_print "*******************************" 16 | exit 1 17 | } 18 | 19 | ######################### 20 | # Load util_functions.sh 21 | ######################### 22 | 23 | OUTFD=$2 24 | ZIPFILE=$3 25 | 26 | mount /data 2>/dev/null 27 | 28 | [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk 29 | . /data/adb/magisk/util_functions.sh 30 | [ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk 31 | 32 | install_module 33 | exit 0 34 | -------------------------------------------------------------------------------- /template/magisk_module/META-INF/com/google/android/updater-script: -------------------------------------------------------------------------------- 1 | #MAGISK 2 | -------------------------------------------------------------------------------- /template/magisk_module/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JingMatrix/FontLoader/6a9cb72f72a4751cd3e2d34548e0ccf65e3a123f/template/magisk_module/README.md -------------------------------------------------------------------------------- /template/magisk_module/customize.sh: -------------------------------------------------------------------------------- 1 | SKIPUNZIP=1 2 | 3 | # Extract verify.sh 4 | ui_print "- Extracting verify.sh" 5 | unzip -o "$ZIPFILE" 'verify.sh' -d "$TMPDIR" >&2 6 | if [ ! -f "$TMPDIR/verify.sh" ]; then 7 | ui_print "*********************************************************" 8 | ui_print "! Unable to extract verify.sh!" 9 | ui_print "! This zip may be corrupted, please try downloading again" 10 | abort "*********************************************************" 11 | fi 12 | . $TMPDIR/verify.sh 13 | 14 | # Extract util_functions.sh 15 | ui_print "- Extracting util_functions.sh" 16 | extract "$ZIPFILE" 'util_functions.sh' "$TMPDIR" 17 | . $TMPDIR/util_functions.sh 18 | 19 | ######################################################### 20 | 21 | enforce_install_from_magisk_app 22 | check_magisk_version 23 | check_android_version 24 | check_arch 25 | 26 | # Extract libs 27 | ui_print "- Extracting module files" 28 | 29 | extract "$ZIPFILE" 'module.prop' "$MODPATH" 30 | extract "$ZIPFILE" 'post-fs-data.sh' "$MODPATH" 31 | 32 | mkdir "$MODPATH/zygisk" 33 | 34 | extract "$ZIPFILE" "lib/$ARCH_NAME/libfontloader.so" "$MODPATH/zygisk" true 35 | mv "$MODPATH/zygisk/libfontloader.so" "$MODPATH/zygisk/$ARCH_NAME.so" 36 | 37 | if [ "$IS64BIT" = true ]; then 38 | extract "$ZIPFILE" "lib/$ARCH_NAME_SECONDARY/libfontloader.so" "$MODPATH/zygisk" true 39 | mv "$MODPATH/zygisk/libfontloader.so" "$MODPATH/zygisk/$ARCH_NAME_SECONDARY.so" 40 | fi 41 | 42 | set_perm_recursive "$MODPATH" 0 0 0755 0644 43 | -------------------------------------------------------------------------------- /template/magisk_module/module.prop: -------------------------------------------------------------------------------- 1 | id=${id} 2 | name=${name} 3 | version=${version} 4 | versionCode=${versionCode} 5 | author=${author} 6 | description=${description} 7 | -------------------------------------------------------------------------------- /template/magisk_module/post-fs-data.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | MODDIR=${0%/*} 3 | MODULE_ID=$(basename "$MODDIR") 4 | 5 | MAGISK_VER_CODE=$(magisk -V) 6 | if [ "$MAGISK_VER_CODE" -ge 21000 ]; then 7 | MAGISK_PATH="$(magisk --path)/.magisk/modules/$MODULE_ID" 8 | else 9 | MAGISK_PATH=/sbin/.magisk/modules/$MODULE_ID 10 | fi 11 | 12 | log -p i -t "FontLoader" "Magisk version $MAGISK_VER_CODE" 13 | log -p i -t "FontLoader" "Magisk module path $MAGISK_PATH" 14 | -------------------------------------------------------------------------------- /template/magisk_module/util_functions.sh: -------------------------------------------------------------------------------- 1 | if [ "$ARCH" = "arm64" ]; then 2 | ARCH_NAME="arm64-v8a" 3 | ARCH_NAME_SECONDARY="armeabi-v7a" 4 | ARCH_DIR="lib64" 5 | ARCH_DIR_SECONDARY="lib" 6 | elif [ "$ARCH" = "arm" ]; then 7 | ARCH_NAME="armeabi-v7a" 8 | ARCH_DIR="lib" 9 | elif [ "$ARCH" = "x64" ]; then 10 | ARCH_NAME="x86_64" 11 | ARCH_NAME_SECONDARY="x86" 12 | ARCH_DIR="lib64" 13 | ARCH_DIR_SECONDARY="lib" 14 | elif [ "$ARCH" = "x86" ]; then 15 | ARCH_NAME="x86" 16 | ARCH_DIR="lib" 17 | fi 18 | 19 | enforce_install_from_magisk_app() { 20 | if [ ! "$BOOTMODE" ]; then 21 | ui_print "*********************************************************" 22 | ui_print "! Install from recovery is NOT supported" 23 | ui_print "! Some recovery has broken implementations, install with such recovery will finally cause module not working" 24 | ui_print "! Please install from Magisk app" 25 | abort "*********************************************************" 26 | fi 27 | } 28 | 29 | check_arch() { 30 | if [ -z $ARCH_NAME ]; then 31 | abort "! Unsupported platform: $ARCH" 32 | else 33 | ui_print "- Device platform: $ARCH" 34 | fi 35 | } 36 | 37 | check_android_version() { 38 | if [ "$API" -ge 31 ]; then 39 | ui_print "- Android SDK version: $API" 40 | else 41 | ui_print "*********************************************************" 42 | ui_print "! Requires Android 12 (API 31) or above" 43 | abort "*********************************************************" 44 | fi 45 | } 46 | 47 | check_magisk_version() { 48 | ui_print "- Magisk version: $MAGISK_VER ($MAGISK_VER_CODE)" 49 | ui_print "- Installing Font Loader" 50 | 51 | if [ "$MAGISK_VER_CODE" -lt 23014 ]; then 52 | ui_print "*********************************************************" 53 | ui_print "! Zygisk requires Magisk 23014+" 54 | abort "*********************************************************" 55 | fi 56 | } 57 | -------------------------------------------------------------------------------- /template/magisk_module/verify.sh: -------------------------------------------------------------------------------- 1 | TMPDIR_FOR_VERIFY="$TMPDIR/.vunzip" 2 | mkdir "$TMPDIR_FOR_VERIFY" 3 | 4 | abort_verify() { 5 | ui_print "*********************************************************" 6 | ui_print "! $1" 7 | ui_print "! This zip may be corrupted, please try downloading again" 8 | abort "*********************************************************" 9 | } 10 | 11 | # extract 12 | extract() { 13 | zip=$1 14 | file=$2 15 | dir=$3 16 | junk_paths=$4 17 | [ -z "$junk_paths" ] && junk_paths=false 18 | opts="-o" 19 | [ $junk_paths = true ] && opts="-oj" 20 | 21 | file_path="" 22 | hash_path="" 23 | if [ $junk_paths = true ]; then 24 | file_path="$dir/$(basename "$file")" 25 | hash_path="$TMPDIR_FOR_VERIFY/$(basename "$file").sha256sum" 26 | else 27 | file_path="$dir/$file" 28 | hash_path="$TMPDIR_FOR_VERIFY/$file.sha256sum" 29 | fi 30 | 31 | unzip $opts "$zip" "$file" -d "$dir" >&2 32 | [ -f "$file_path" ] || abort_verify "$file not exists" 33 | 34 | unzip $opts "$zip" "$file.sha256sum" -d "$TMPDIR_FOR_VERIFY" >&2 35 | [ -f "$hash_path" ] || abort_verify "$file.sha256sum not exists" 36 | 37 | (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" 38 | ui_print "- Verified $file" >&1 39 | } 40 | --------------------------------------------------------------------------------