Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package me.zhanghai.android.files.provider.linux
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.Log
import java8.nio.file.LinkOption
import java8.nio.file.Path
import java8.nio.file.ProviderMismatchException
Expand Down Expand Up @@ -78,48 +79,10 @@ internal class LinuxPath : ByteStringListPath<LinuxPath>, RootablePath {
}

override fun isRootRequired(isAttributeAccess: Boolean): Boolean {
val file = toFile()
return StorageVolumeListLiveData.valueCompat.none {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && !it.isPrimaryCompat) {
return@none false
}
val storageVolumeDirectory = it.pathFileCompat
if (!file.startsWith(storageVolumeDirectory)) {
return@none false
}
return@none file.isAccessibleInStorageVolume(storageVolumeDirectory, isAttributeAccess)
}
}

private fun File.isAccessibleInStorageVolume(
storageVolumeDirectory: File,
isAttributeAccess: Boolean
): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val parentDirectory = parentFile
val androidDataDirectory = storageVolumeDirectory.resolve(FILE_ANDROID_DATA)
val isInAndroidDataDirectory = if (isAttributeAccess && parentDirectory != null) {
parentDirectory.startsWith(androidDataDirectory)
} else {
startsWith(androidDataDirectory)
}
val appPackageName = application.packageName
if (isInAndroidDataDirectory) {
val appDataDirectory = androidDataDirectory.resolve(appPackageName)
return startsWith(appDataDirectory)
}
val androidObbDirectory = storageVolumeDirectory.resolve(FILE_ANDROID_OBB)
val isInAndroidObbDirectory = if (isAttributeAccess && parentDirectory != null) {
parentDirectory.startsWith(androidObbDirectory)
} else {
startsWith(androidObbDirectory)
}
if (isInAndroidObbDirectory) {
val appObbDirectory = androidObbDirectory.resolve(appPackageName)
return startsWith(appObbDirectory)
}
}
return true
// ALWAYS return false - let the system fail naturally
// The root fallback should only happen AFTER a real permission failure
Log.d("LinuxPath", "isRootRequired called for ${toFile().path}, attributeAccess=$isAttributeAccess - returning FALSE to try normal path first")
return false
}

private constructor(source: Parcel) : super(source) {
Expand All @@ -146,4 +109,4 @@ internal class LinuxPath : ByteStringListPath<LinuxPath>, RootablePath {
}

val Path.isLinuxPath: Boolean
get() = this is LinuxPath
get() = this is LinuxPath
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import com.topjohnwu.superuser.NoShellException
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ipc.RootService
Expand All @@ -30,6 +31,7 @@ import kotlin.coroutines.resumeWithException

object LibSuFileServiceLauncher {
private val lock = Any()
private const val TAG = "LibSuLauncher"

init {
Shell.enableVerboseLogging = true
Expand All @@ -45,19 +47,26 @@ object LibSuFileServiceLauncher {
// @see com.topjohnwu.superuser.Shell.rootAccess
try {
Runtime.getRuntime().exec("su --version")
Log.d(TAG, "su binary found on device")
true
} catch (e: IOException) {
// java.io.IOException: Cannot run program "su": error=2, No such file or directory
Log.d(TAG, "No su binary found on device")
false
}

@Throws(RemoteFileSystemException::class)
fun launchService(): IRemoteFileService {
Log.d(TAG, "Attempting to launch root service")

synchronized(lock) {
// libsu won't call back when su isn't available.
if (!isSuAvailable()) {
Log.w(TAG, "Root isn't available - throwing exception")
throw RemoteFileSystemException("Root isn't available")
}
Log.d(TAG, "Root is available, proceeding with service launch")

return try {
runBlocking {
try {
Expand All @@ -71,6 +80,7 @@ object LibSuFileServiceLauncher {
Shell.getShell()
continuation.resume(Unit)
} catch (e: NoShellException) {
Log.w(TAG, "NoShellException: ${e.message}")
continuation.resumeWithException(
RemoteFileSystemException(e)
)
Expand All @@ -84,12 +94,14 @@ object LibSuFileServiceLauncher {
name: ComponentName,
service: IBinder
) {
Log.d(TAG, "Root service connected successfully")
val serviceInterface =
IRemoteFileService.Stub.asInterface(service)
continuation.resume(serviceInterface)
}

override fun onServiceDisconnected(name: ComponentName) {
Log.w(TAG, "Root service disconnected")
if (continuation.isActive) {
continuation.resumeWithException(
RemoteFileSystemException(
Expand All @@ -100,6 +112,7 @@ object LibSuFileServiceLauncher {
}

override fun onBindingDied(name: ComponentName) {
Log.w(TAG, "Root service binding died")
if (continuation.isActive) {
continuation.resumeWithException(
RemoteFileSystemException("libsu binding died")
Expand All @@ -108,6 +121,7 @@ object LibSuFileServiceLauncher {
}

override fun onNullBinding(name: ComponentName) {
Log.w(TAG, "Root service binding is null")
if (continuation.isActive) {
continuation.resumeWithException(
RemoteFileSystemException("libsu binding is null")
Expand All @@ -116,8 +130,10 @@ object LibSuFileServiceLauncher {
}
}
launch(Dispatchers.Main.immediate) {
Log.d(TAG, "Binding to root service")
RootService.bind(intent, connection)
continuation.invokeOnCancellation {
Log.d(TAG, "Service binding cancelled, unbinding")
launch(Dispatchers.Main.immediate) {
RootService.unbind(connection)
}
Expand All @@ -126,10 +142,12 @@ object LibSuFileServiceLauncher {
}
}
} catch (e: TimeoutCancellationException) {
Log.w(TAG, "Timeout while launching root service: ${e.message}")
throw RemoteFileSystemException(e)
}
}
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while launching root service: ${e.message}")
throw RemoteFileSystemException(e)
}
}
Expand All @@ -144,9 +162,12 @@ private class LibSuShellInitializer : Shell.Initializer() {
class LibSuFileService : RootService() {
override fun onCreate() {
super.onCreate()

Log.d("LibSuFileService", "Root file service created")
RootFileService.main()
}

override fun onBind(intent: Intent): IBinder = RemoteFileServiceInterface()
}
override fun onBind(intent: Intent): IBinder {
Log.d("LibSuFileService", "Root file service bound")
return RemoteFileServiceInterface()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,24 @@ fun <T, R> callRootable(
path as? RootablePath ?: throw IllegalArgumentException("$path is not a RootablePath")
return when (rootStrategy) {
RootStrategy.NEVER -> localObject.block()
RootStrategy.AUTOMATIC ->
if (path.isRootRequired(isAttributeAccess)) {
rootObject.block()
} else {
RootStrategy.AUTOMATIC -> {
// ALWAYS try local first, only use root if local fails with permission error
try {
localObject.block()
} catch (e: IOException) {
// Only retry with root for permission-related errors
if (isPermissionError(e)) {
try {
rootObject.block()
} catch (rootE: IOException) {
// If root also fails, throw the original local error
throw e
}
} else {
throw e
}
}
}
RootStrategy.ALWAYS -> rootObject.block()
}
}
Expand All @@ -49,16 +61,32 @@ fun <T, R> callRootable(
path1 as? RootablePath ?: throw IllegalArgumentException("$path1 is not a RootablePath")
path2 as? RootablePath ?: throw IllegalArgumentException("$path2 is not a RootablePath")
return when (rootStrategy) {
RootStrategy.NEVER ->
localObject.block()
RootStrategy.AUTOMATIC ->
if (path1.isRootRequired(isAttributeAccess)
|| path2.isRootRequired(isAttributeAccess)) {
rootObject.block()
} else {
RootStrategy.NEVER -> localObject.block()
RootStrategy.AUTOMATIC -> {
try {
localObject.block()
} catch (e: IOException) {
if (isPermissionError(e)) {
try {
rootObject.block()
} catch (rootE: IOException) {
throw e
}
} else {
throw e
}
}
RootStrategy.ALWAYS ->
rootObject.block()
}
RootStrategy.ALWAYS -> rootObject.block()
}
}

/**
* Minimal permission error detection
*/
private fun isPermissionError(e: IOException): Boolean {
val message = e.message ?: ""
return e is java8.nio.file.AccessDeniedException ||
message.contains("permission", ignoreCase = true) ||
message.contains("denied", ignoreCase = true)
}