From bec5edca84058c8c054883cde2470c850884e583 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Fri, 29 Jan 2021 05:15:22 -0800 Subject: [PATCH] Avoiding using shell I/O --- .../java/com/topjohnwu/magisk/core/App.kt | 5 +- .../java/com/topjohnwu/magisk/core/Info.kt | 4 +- .../magisk/core/tasks/MagiskInstaller.kt | 231 ++++++++++-------- .../core/utils/{RootInit.kt => ShellInit.kt} | 63 +++-- app/src/main/res/raw/manager.sh | 19 +- gradle.properties | 2 +- 6 files changed, 192 insertions(+), 132 deletions(-) rename app/src/main/java/com/topjohnwu/magisk/core/utils/{RootInit.kt => ShellInit.kt} (65%) diff --git a/app/src/main/java/com/topjohnwu/magisk/core/App.kt b/app/src/main/java/com/topjohnwu/magisk/core/App.kt index 285277ec1..1348caa4e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/App.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/App.kt @@ -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() diff --git a/app/src/main/java/com/topjohnwu/magisk/core/Info.kt b/app/src/main/java/com/topjohnwu/magisk/core/Info.kt index 298927274..4c1fd6ac8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/Info.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/Info.kt @@ -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 -> diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt index df37e396d..d4208677b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt @@ -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 { - "cp_readlink $installDir".sh() - } + 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.sh() = Shell.sh(*this).to(console, logs).exec() - private fun String.fsh() = ShellUtils.fastCmd(this) - private fun Array.fsh() = ShellUtils.fastCmd(*this) + private fun String.sh() = shell.newJob().add(this).to(console, logs).exec() + private fun Array.sh() = shell.newJob().add(*this).to(console, logs).exec() + private fun String.fsh() = ShellUtils.fastCmd(shell, this) + private fun Array.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( diff --git a/app/src/main/java/com/topjohnwu/magisk/core/utils/RootInit.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/ShellInit.kt similarity index 65% rename from app/src/main/java/com/topjohnwu/magisk/core/utils/RootInit.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/ShellInit.kt index ac1a8f3b7..177ae2fa3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/utils/RootInit.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/ShellInit.kt @@ -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") diff --git a/app/src/main/res/raw/manager.sh b/app/src/main/res/raw/manager.sh index 4bf5fe311..08881d74f 100644 --- a/app/src/main/res/raw/manager.sh +++ b/app/src/main/res/raw/manager.sh @@ -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 diff --git a/gradle.properties b/gradle.properties index ba351d39d..7284b9514 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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