Avoiding using shell I/O

This commit is contained in:
topjohnwu 2021-01-29 05:15:22 -08:00
parent 6fb20b3ee5
commit bec5edca84
6 changed files with 192 additions and 132 deletions

View File

@ -10,8 +10,9 @@ import androidx.multidex.MultiDex
import androidx.work.WorkManager
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.core.utils.AppShellInit
import com.topjohnwu.magisk.core.utils.BusyBoxInit
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
import com.topjohnwu.magisk.core.utils.RootInit
import com.topjohnwu.magisk.core.utils.updateConfig
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.ktx.unwrap
@ -32,7 +33,7 @@ open class App() : Application() {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
Shell.setDefaultBuilder(Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setInitializers(RootInit::class.java)
.setInitializers(BusyBoxInit::class.java, AppShellInit::class.java)
.setTimeout(2))
Shell.EXECUTOR = IODispatcherExecutor()

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk.core
import android.os.Build
import androidx.databinding.ObservableBoolean
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.core.model.UpdateInfo
@ -34,9 +35,10 @@ object Info {
@JvmStatic val isFDE get() = crypto == "block"
@JvmField var ramdisk = false
@JvmField var hasGMS = true
@JvmField var isPixel = false
@JvmField val isPixel = Build.BRAND == "google"
@JvmField val isEmulator = getProperty("ro.kernel.qemu", "0") == "1"
var crypto = ""
var noDataExec = false
val isConnected by lazy {
ObservableBoolean(false).also { field ->

View File

@ -8,9 +8,9 @@ import androidx.core.os.postDelayed
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.utils.BusyBoxInit
import com.topjohnwu.magisk.core.utils.LightShellInit
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
@ -27,8 +27,6 @@ import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.NOPList
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.jpountz.lz4.LZ4FrameInputStream
@ -53,7 +51,9 @@ abstract class MagiskInstallImpl protected constructor(
protected lateinit var installDir: File
private lateinit var srcBoot: File
private var tarOut: TarOutputStream? = null
private lateinit var shell: Shell
private var closeShell = false
private val service: NetworkService by inject()
protected val context: Context by inject(Protected)
@ -68,7 +68,7 @@ abstract class MagiskInstallImpl protected constructor(
return true
}
private fun findSecondaryImage(): Boolean {
private fun findSecondary(): Boolean {
val slot = "echo \$SLOT".fsh()
val target = if (slot == "_a") "_b" else "_a"
console.add("- Target slot: $target")
@ -86,26 +86,13 @@ abstract class MagiskInstallImpl protected constructor(
return true
}
private fun installDirFile(name: String): File {
return if (installDir is SuFile)
SuFile(installDir, name)
else
File(installDir, name)
}
private fun extractFiles(): Boolean {
console.add("- Device platform: ${Const.CPU_ABI}")
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
val binDir = File(context.filesDir.parent, "install")
binDir.deleteRecursively()
binDir.mkdirs()
installDir = if (Shell.rootAccess()) {
SuFile("${Const.TMPDIR}/install")
} else {
binDir
}
installDir = File(context.filesDir.parent, "install")
installDir.deleteRecursively()
installDir.mkdirs()
try {
// Extract binaries
@ -116,7 +103,7 @@ abstract class MagiskInstallImpl protected constructor(
}.forEach {
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
val name = n.substring(3, n.length - 3)
val dest = File(binDir, name)
val dest = File(installDir, name)
zf.getInputStream(it).writeTo(dest)
}
} else {
@ -125,21 +112,21 @@ abstract class MagiskInstallImpl protected constructor(
} ?: emptyArray()
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3)
val bin = File(binDir, name)
val bin = File(installDir, name)
symlink(lib.path, bin.path)
}
}
// Extract scripts
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
val dest = File(binDir, script)
val dest = File(installDir, script)
context.assets.open(script).writeTo(dest)
}
// Extract chromeos tools
File(binDir, "chromeos").mkdir()
File(installDir, "chromeos").mkdir()
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
val name = "chromeos/$file"
val dest = File(binDir, name)
val dest = File(installDir, name)
context.assets.open(name).writeTo(dest)
}
} catch (e: Exception) {
@ -148,19 +135,10 @@ abstract class MagiskInstallImpl protected constructor(
return false
}
if (installDir !== binDir) {
arrayOf(
"rm -rf $installDir",
"mkdir -p $installDir",
"cp_readlink $binDir $installDir",
"rm -rf $binDir"
).sh()
}
return true
}
private fun newEntry(name: String, size: Long): TarEntry {
private fun newTarEntry(name: String, size: Long): TarEntry {
console.add("-- Writing: $name")
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
}
@ -172,8 +150,13 @@ abstract class MagiskInstallImpl protected constructor(
TarInputStream(input).use { tarIn ->
lateinit var entry: TarEntry
fun decompressedStream() =
if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
fun decompressedStream(): InputStream {
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
return object : FilterInputStream(src) {
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
override fun close() { /* Never close src stream */ }
}
}
while (tarIn.nextEntry?.let { entry = it } != null) {
if (entry.name.contains("boot.img") ||
@ -181,15 +164,10 @@ abstract class MagiskInstallImpl protected constructor(
val name = entry.name.replace(".lz4", "")
console.add("-- Extracting: $name")
val extract = installDirFile(name)
SuFileOutputStream(extract).use { decompressedStream().copyTo(it) }
val extract = File(installDir, name)
decompressedStream().writeTo(extract)
} else if (entry.name.contains("vbmeta.img")) {
// DO NOT USE readBytes() DUE TO BUG IN LZ4FrameInputStream
val rawData = decompressedStream().run {
val buffer = ByteArrayOutputStream()
copyTo(buffer)
buffer.toByteArray()
}
val rawData = decompressedStream().readBytes()
// Valid vbmeta.img should be at least 256 bytes
if (rawData.size < 256)
continue
@ -198,7 +176,7 @@ abstract class MagiskInstallImpl protected constructor(
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
console.add("-- Patching: vbmeta.img")
ByteBuffer.wrap(rawData).putInt(120, 3)
tarOut.putNextEntry(newEntry("vbmeta.img", rawData.size.toLong()))
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
tarOut.write(rawData)
} else {
console.add("-- Copying: ${entry.name}")
@ -207,19 +185,21 @@ abstract class MagiskInstallImpl protected constructor(
}
}
}
val boot = installDirFile("boot.img")
val recovery = installDirFile("recovery.img")
val boot = File(installDir, "boot.img")
val recovery = File(installDir, "recovery.img")
if (Config.recovery && recovery.exists() && boot.exists()) {
// Install Magisk to recovery
// Install to recovery
srcBoot = recovery
// Repack boot image to prevent restore
// Repack boot image to prevent auto restore
arrayOf(
"cd $installDir",
"./magiskboot unpack boot.img",
"./magiskboot repack boot.img",
"./magiskboot cleanup",
"mv new-boot.img boot.img").sh()
SuFileInputStream(boot).use {
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
"mv new-boot.img boot.img",
"cd /").sh()
boot.inputStream().use {
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
it.copyTo(tarOut)
}
boot.delete()
@ -263,9 +243,9 @@ abstract class MagiskInstallImpl protected constructor(
handleTar(src, outFile!!.uri.outputStream())
} else {
// Raw image
srcBoot = installDirFile("boot.img")
srcBoot = File(installDir, "boot.img")
console.add("- Copying image to cache")
SuFileOutputStream(srcBoot).use { src.copyTo(it) }
src.writeTo(srcBoot)
outFile = MediaStoreUtils.getFile("$filename.img", true)
outFile!!.uri.outputStream()
}
@ -285,12 +265,12 @@ abstract class MagiskInstallImpl protected constructor(
// Output file
try {
val patched = installDirFile("new-boot.img")
val patched = File(installDir, "new-boot.img")
if (outStream is TarOutputStream) {
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
outStream.putNextEntry(newEntry(name, patched.length()))
outStream.putNextEntry(newTarEntry(name, patched.length()))
}
withStreams(SuFileInputStream(patched), outStream) { src, out -> src.copyTo(out) }
withStreams(patched.inputStream(), outStream) { src, out -> src.copyTo(out) }
patched.delete()
console.add("")
@ -306,22 +286,56 @@ abstract class MagiskInstallImpl protected constructor(
}
// Fix up binaries
if (installDir is SuFile) {
"fix_env $installDir".sh()
} else {
srcBoot.delete()
"cp_readlink $installDir".sh()
}
return true
}
private fun rootInputStream(file: File): InputStream {
return if (file is SuFile)
object : BufferedInputStream(null) {
private val tmp =
File.createTempFile(file.name, null, context.cacheDir).also {
// Copy to tmp file and read from there
"cat $file > $it".sh()
`in` = it.inputStream()
}
override fun close() {
super.close()
tmp.delete()
}
}
else
file.inputStream().buffered()
}
private fun patchBoot(): Boolean {
"cd $installDir".sh()
val inRootDir = shell.isRoot && Info.noDataExec
val newBootImg: File
if (inRootDir) {
// Migrate everything to tmpfs to workaround Samsung bullshit
SuFile("${Const.TMPDIR}/install").also {
arrayOf(
"rm -rf $it",
"mkdir -p $it",
"cp_readlink $installDir $it",
"rm -rf $installDir"
).sh()
installDir = it
}
newBootImg = SuFile(installDir, "new-boot.img")
} else {
newBootImg = File(installDir, "new-boot.img")
// Create the output file before hand
newBootImg.createNewFile()
}
var isSigned = false
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
try {
SuFileInputStream(srcBoot).use {
rootInputStream(srcBoot).use {
if (SignBoot.verifySignature(it, null)) {
isSigned = true
console.add("- Boot image is signed with AVB 1.0")
@ -333,90 +347,99 @@ abstract class MagiskInstallImpl protected constructor(
}
}
val FLAGS =
val flags =
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
"KEEPVERITY=${Config.keepVerity} " +
"RECOVERYMODE=${Config.recovery}"
if (!"$FLAGS sh boot_patch.sh $srcBoot".sh().isSuccess)
if (!"cd $installDir; $flags sh boot_patch.sh $srcBoot".sh().isSuccess)
return false
val job = Shell.sh("./magiskboot cleanup", "cd /")
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
val patched = installDirFile("new-boot.img")
if (isSigned) {
console.add("- Signing boot image with verity keys")
val signed = installDirFile("signed.img")
val signed = File.createTempFile("signed", ".img", context.cacheDir)
try {
withStreams(SuFileInputStream(patched), SuFileOutputStream(signed)) {
input, out -> SignBoot.doSignature(null, null, input, out, "/boot")
withStreams(rootInputStream(newBootImg), signed.outputStream().buffered()) {
src, out -> SignBoot.doSignature(null, null, src, out, "/boot")
}
} catch (e: IOException) {
console.add("! Unable to sign image")
Timber.e(e)
return false
}
job.add("mv -f $signed $patched")
if (inRootDir) {
job.add("cp -f $signed $newBootImg", "rm -f $signed")
} else {
signed.copyTo(newBootImg, overwrite = true)
signed.delete()
}
}
job.exec()
return true
}
private fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false
arrayOf("run_migrations", "copy_sepolicy_rules").sh()
return true
}
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
private suspend fun postOTA(): Boolean {
val bootctl = SuFile("/data/adb/bootctl")
try {
withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
it, out -> it.copyTo(out)
}
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
service.fetchBootctl().byteStream().writeTo(bootctl)
"post_ota $bootctl".sh()
} catch (e: IOException) {
console.add("! Unable to download bootctl")
Timber.e(e)
return false
}
"post_ota ${bootctl.parent}".sh()
console.add("***************************************")
console.add(" Next reboot will boot to second slot!")
console.add("***************************************")
return true
}
protected fun uninstall(): Boolean {
val apk = if (isRunningAsStub) {
DynAPK.current(context).path
} else {
context.packageCodePath
private fun setupShell(forceNonRoot: Boolean = false): Boolean {
shell = Shell.getShell()
if (forceNonRoot && shell.isRoot) {
shell = Shell.Builder.create()
.setFlags(Shell.FLAG_NON_ROOT_SHELL)
.setInitializers(BusyBoxInit::class.java, LightShellInit::class.java)
.build()
closeShell = true
}
return "run_uninstaller $apk".sh().isSuccess
return true
}
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
private fun String.fsh() = ShellUtils.fastCmd(this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
protected fun doPatchFile(patchFile: Uri) =
setupShell(true) && extractFiles() && handleFile(patchFile)
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
protected fun direct() =
setupShell() && findImage() && extractFiles() && patchBoot() && flashBoot()
protected suspend fun secondSlot() =
findSecondaryImage() && extractFiles() && patchBoot() && flashBoot() && postOTA()
setupShell() && findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
protected fun fixEnv() =
setupShell() && extractFiles() && "fix_env $installDir".sh().isSuccess
protected fun uninstall() =
setupShell() && "run_uninstaller ${AssetHack.apk}".sh().isSuccess
@WorkerThread
protected abstract suspend fun operations(): Boolean
open suspend fun exec() = withContext(Dispatchers.IO) { operations() }
open suspend fun exec() = withContext(Dispatchers.IO) {
val result = operations()
if (closeShell)
shell.close()
result
}
}
abstract class MagiskInstaller(

View File

@ -14,20 +14,19 @@ import com.topjohnwu.superuser.ShellUtils
import java.io.File
import java.util.jar.JarFile
class RootInit : Shell.Initializer() {
override fun onInit(context: Context, shell: Shell): Boolean {
abstract class BaseShellInit : Shell.Initializer() {
final override fun onInit(context: Context, shell: Shell): Boolean {
return init(context.wrap(), shell)
}
private fun init(context: Context, shell: Shell): Boolean {
abstract fun init(context: Context, shell: Shell): Boolean
}
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
fun getVar(name: String) = fastCmd("echo \$$name")
fun getBool(name: String) = getVar(name).toBoolean()
class BusyBoxInit : BaseShellInit() {
override fun init(context: Context, shell: Shell): Boolean {
shell.newJob().apply {
add("export SDK_INT=${Build.VERSION.SDK_INT}")
add("export ASH_STANDALONE=1")
val localBB: File
@ -41,27 +40,58 @@ class RootInit : Shell.Initializer() {
localBB = File(Const.NATIVE_LIB_DIR, "libbusybox.so")
}
if (!shell.isRoot) {
// Directly execute the file
add("exec $localBB sh")
} else {
// Copy it out of /data to workaround Samsung BS
if (shell.isRoot) {
add("export MAGISKTMP=\$(magisk --path)/.magisk")
// Test if we can properly execute stuff in /data
Info.noDataExec = !shell.newJob().add("$localBB true").exec().isSuccess
}
if (Info.noDataExec) {
// Copy it out of /data to workaround Samsung bullshit
add(
"export MAGISKTMP=\$(magisk --path)/.magisk",
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
" exec \$MAGISKTMP/busybox/busybox sh",
"else",
" exec $localBB sh",
" cp -af $localBB /dev/.busybox",
" exec /dev/.busybox sh",
"fi"
)
} else {
// Directly execute the file
add("exec $localBB sh")
}
}.exec()
return true
}
}
class LightShellInit : BaseShellInit() {
override fun init(context: Context, shell: Shell): Boolean {
shell.newJob().apply {
add("export SDK_INT=${Build.VERSION.SDK_INT}")
add(context.rawResource(R.raw.manager))
}.exec()
return true
}
}
class AppShellInit : BaseShellInit() {
override fun init(context: Context, shell: Shell): Boolean {
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
fun getVar(name: String) = fastCmd("echo \$$name")
fun getBool(name: String) = getVar(name).toBoolean()
shell.newJob().apply {
add("export SDK_INT=${Build.VERSION.SDK_INT}")
add(context.rawResource(R.raw.manager))
if (shell.isRoot) {
add(context.assets.open("util_functions.sh"))
}
add("mm_init")
add("app_init")
}.exec()
Const.MAGISKTMP = getVar("MAGISKTMP")
@ -69,7 +99,6 @@ class RootInit : Shell.Initializer() {
Info.ramdisk = getBool("RAMDISKEXIST")
Info.isAB = getBool("ISAB")
Info.crypto = getVar("CRYPTOTYPE")
Info.isPixel = fastCmd("getprop ro.product.brand") == "google"
// Default presets
Config.recovery = getBool("RECOVERYMODE")

View File

@ -36,7 +36,7 @@ fix_env() {
rm -rf $MAGISKBIN/*
mkdir -p $MAGISKBIN 2>/dev/null
chmod 700 $NVBASE
cp -af $1/. $MAGISKBIN
cp_readlink $1 $MAGISKBIN
rm -rf $1
chown -R 0:0 $MAGISKBIN
}
@ -57,6 +57,8 @@ direct_install() {
rm -f $1/new-boot.img
fix_env $1
run_migrations
copy_sepolicy_rules
return 0
}
@ -87,15 +89,17 @@ restore_imgs() {
}
post_ota() {
cd $1
cd /data/adb
cp -f $1 bootctl
rm -f $1
chmod 755 bootctl
./bootctl hal-info || return
[ $(./bootctl get-current-slot) -eq 0 ] && SLOT_NUM=1 || SLOT_NUM=0
./bootctl set-active-boot-slot $SLOT_NUM
cat << EOF > post-fs-data.d/post_ota.sh
${1}/bootctl mark-boot-successful
rm -f ${1}/bootctl
rm -f ${1}/post-fs-data.d/post_ota.sh
/data/adb/bootctl mark-boot-successful
rm -f /data/adb/bootctl
rm -f /data/adb/post-fs-data.d/post_ota.sh
EOF
chmod 755 post-fs-data.d/post_ota.sh
cd /
@ -196,8 +200,7 @@ grep_prop() { return; }
# Initialize
#############
mm_init() {
export BOOTMODE=true
app_init() {
mount_partitions
get_flags
run_migrations
@ -207,3 +210,5 @@ mm_init() {
# Make sure RECOVERYMODE has value
[ -z $RECOVERYMODE ] && RECOVERYMODE=false
}
export BOOTMODE=true

View File

@ -28,6 +28,6 @@ kapt.incremental.apt=true
# Magisk
magisk.stubVersion=17
magisk.versionCode=21405
magisk.versionCode=21406
magisk.ndkVersion=21d
magisk.fullNdkVersion=21.3.6528147