├── .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 |
--------------------------------------------------------------------------------