android-discuss icon indicating copy to clipboard operation
android-discuss copied to clipboard

各位遇到过对自己的内部存储空间没有访问权限的情况吗?

Open hydraxman opened this issue 5 years ago • 16 comments

线上问题,线下无法复现:App对自己的internal storage data目录没有访问权限: 这个是我们的线上log: filePath: /data/user/0/packageName/databases/app_db.db pFUTSpace: 0,0,0 fileExist: false comm: ls -Zl /data/user/0/packageName/databases/app_db.db errorCode: 1 lsErrorOutput: ls: /data/user/0/packageName/databases/app_db.db: Permission denied . comm: chmod -R 770 /data/user/0/packageName/databases/app_db.db errorCode: 1 chmodErrorOutput: chmod: /data/user/0/packageName/databases/app_db.db: Permission denied . comm: restorecon -RF /data/user/0/packageName/databases/app_db.db restoreconOutput: . chmodRetried: true filePath: /data/user/0/packageName/databases pFUTSpace: 0,0,0 fileExist: false comm: ls -Zl /data/user/0/packageName/databases errorCode: 1 lsErrorOutput: ls: /data/user/0/packageName/databases: Permission denied . comm: chmod -R 770 /data/user/0/packageName/databases errorCode: 1 chmodErrorOutput: chmod: /data/user/0/packageName/databases: Permission denied . comm: restorecon -RF /data/user/0/packageName/databases restoreconOutput: . chmodRetried: true filePath: /data/user/0/packageName pFUTSpace: 1637703680,1620926464,54091657216 fileExist: true fileCanRead: false fileCanWrite: false fileCanExecute: false fileLength: 4096 fileIsDirectory: true comm: ls -Zl /data/user/0/packageName errorCode: 1 lsErrorOutput: ls: /data/user/0/packageName: Permission denied . comm: chmod -R 770 /data/user/0/packageName errorCode: 1 chmodErrorOutput: chmod: chmod '/data/user/0/packageName' to 40770: Operation not permitted chmod: No /data/user/0/packageName: Permission denied . comm: restorecon -RF /data/user/0/packageName restoreconOutput: . chmodRetried: true filePath: /data/user/0/packageName/files pFUTSpace: 0,0,0 fileExist: false comm: ls -Zl /data/user/0/packageName/files errorCode: 1 lsErrorOutput: ls: /data/user/0/packageName/files: Permission denied . comm: chmod -R 770 /data/user/0/packageName/files errorCode: 1 chmodErrorOutput: chmod: /data/user/0/packageName/files: Permission denied . comm: restorecon -RF /data/user/0/packageName/files restoreconOutput: . chmodRetried: true filePath: /data/user/0/packageName/cache pFUTSpace: 0,0,0 fileExist: false comm: ls -Zl /data/user/0/packageName/cache errorCode: 1 lsErrorOutput: ls: /data/user/0/packageName/cache: Permission denied . comm: chmod -R 770 /data/user/0/packageName/cache errorCode: 1 chmodErrorOutput: chmod: /data/user/0/packageName/cache: Permission denied . comm: restorecon -RF /data/user/0/packageName/cache restoreconOutput: . chmodRetried: true

这个问题应该和SELinux(SEAndroid)有关

hydraxman avatar Apr 15 '19 06:04 hydraxman

这个日志的格式、来源是什么

ls命令没有-z参数

执行ls、chmod命令的进程是什么

AndroidInternal avatar Apr 15 '19 09:04 AndroidInternal

这个日志的格式、来源是什么

ls命令没有-z参数

执行ls、chmod命令的进程是什么

@AndroidInternal 有-Z参数,-Z用来查看SEAndroid的文件的SEContext tag 日志是线上崩溃log发到我们服务器上的,执行的进程是我们的App,App没有root权限,就是一般的app

hydraxman avatar Apr 16 '19 03:04 hydraxman

执行命令的应该是一个新的执行shell命令的单独的process吧,没办法直接在app所在进程执行shell命令。

AndroidInternal avatar Apr 17 '19 08:04 AndroidInternal

还有就是,这里面的“packageName”不是真正的包名,不知道是上传上来就是这样,还是你脱敏处理了。

AndroidInternal avatar Apr 17 '19 08:04 AndroidInternal

https://happybevis.github.io/2018/05/02/The-Magic-Selinux-Restore-Rule/

这篇文章讲到了一个大概由于用户升级导致的selinux权限变更

AndroidInternal avatar Apr 17 '19 08:04 AndroidInternal

https://happybevis.github.io/2018/05/02/The-Magic-Selinux-Restore-Rule/

这篇文章讲到了一个大概由于用户升级导致的selinux权限变更

  1. 可以在app进程执行shell啊,用Runtime API
  2. 脱敏了
  3. 这篇文章我也看过了,感谢你帮忙查看。推测是文章中的原因,但是由于是线上日志,无法证明就是root cause。而且针对这种情况,作为APP进程似乎无法修复。

我们这边有几十台线上的用户手机每天报几万条这个crash: SQLiteCantOpenDatabaseException 原因就是app自己因为没有权限读不到自己内部存储的sqlite数据库,非常诡异。

hydraxman avatar Apr 17 '19 09:04 hydraxman

收集崩溃log的代码

package packagename.report;

import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.os.SystemClock;

import packagename.LauncherProvider;
import packagename.next.utils.ErrorReportUtils;
import packagename.timeline.TimelineUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Random;

import androidx.annotation.Keep;

/**
 * @author SHBU
 */
public class SqlCantOpenCrashAnalyzer {
    private static boolean chmodRetried = false;

    /**
     * a workaround for online crash: sql open exception
     *
     * @param context
     */
    public static void uploadDatabaseFileInfoAndRetry(Context context, String dbName, SQLiteCantOpenDatabaseException sqlCantOpen) {
        // collect file info
        StringBuilder stringBuilder = new StringBuilder();
        int retryTime = 0;
        try {
            String key = "retryTime";
            SharedPreferences testSP = context.getSharedPreferences("sqlCrashTestSP", Context.MODE_PRIVATE);
            retryTime = testSP.getInt(key, 0);
            testSP.edit().putInt(key, ++retryTime).commit();
            stringBuilder.append("\nretryTime:").append(retryTime);
        } catch (Throwable e) {
            e.printStackTrace();
            stringBuilder.append("\nSPExpt:").append(e.getMessage()).append(",type:").append(e.getClass().getName());
        }
        if (retryTime > 8) {
            android.os.Process.killProcess(android.os.Process.myPid());
            return;
        }
        // abs path should be /data/user/0/packagename/databases/launcher.db
        File databaseFile = context.getDatabasePath(dbName);
        // abs path should be /data/user/0/packagename/files
        File filesDir = context.getFilesDir();
        // abs path should be /data/user/0/packagename/cache
        File cacheDir = context.getCacheDir();
        File eCacheDir = context.getExternalCacheDir();
        File userDir = new File("/data/user");
        // abs path should be /data/user/0/packagename/databases
        File databaseDir = databaseFile.getParentFile();
        // abs path should be /data/user/0/packagename
        File homeDir = databaseDir.getParentFile();
        // abs path should be /data/user/0/packagename
        File homeDir1 = filesDir.getParentFile();

//        runCommAndLog("getenforce", "getenforce", stringBuilder);// no permission for exec this command
        logFileInfoAndTryChmodForDir(context, databaseFile, stringBuilder);
        logFileInfoAndTryChmodForDir(context, databaseDir, stringBuilder);
        logFileInfoAndTryChmodForDir(context, homeDir, stringBuilder);
        logFileInfoAndTryChmodForDir(context, filesDir, stringBuilder);
        if (!homeDir.getAbsolutePath().equals(homeDir1.getAbsolutePath())) {
            logFileInfoAndTryChmodForDir(context, homeDir1, stringBuilder);
        }
        logFileInfoAndTryChmodForDir(context, cacheDir, stringBuilder);
        logFileInfoAndTryChmodForDir(context, eCacheDir, stringBuilder);


        boolean fileExist = databaseFile.exists();
        boolean deleteSucc = false;
        if (fileExist) {
            boolean fileIsDirectory = databaseFile.isDirectory();
            if (fileIsDirectory) {
                boolean fileIsDirectoryDeletable = databaseFile.delete();
                stringBuilder.append("\ndbFileDirectoryDeletable: ").append(fileIsDirectoryDeletable);
                if (fileIsDirectoryDeletable) {
                    deleteSucc = true;
                }
            } else {
                // try delete db file
                try {
                    deleteSucc = databaseFile.delete();
                    stringBuilder.append("\ndbFileCanDelete: ").append(deleteSucc);
                    // if rename strategy once worked and try rename it to the original one
                    if (deleteSucc && !LauncherProvider.useNewDBName) {
                        File dbFile = context.getDatabasePath(LauncherProvider.DATABASE_NAME_NEW);
                        if (dbFile.exists()) {
                            boolean renameTo = dbFile.renameTo(context.getDatabasePath(LauncherProvider.DATABASE_NAME));
                            stringBuilder.append("\ndbFileCanRenameTo: ").append(renameTo);
                        }
                    }
                } catch (Throwable e) {
                    stringBuilder.append("\ndbFileCanDelete: ").append("exception:").append(e.getClass().getName()).append("-").append(e.getMessage());
                }
            }
        }
        String timelineEnabled;
        try {
            boolean timelineOn = TimelineUtils.isTimelineEnabled(context);
            if (timelineOn) {
                timelineEnabled = "y";
            } else {
                timelineEnabled = "n";
            }
        } catch (Throwable e) {
            timelineEnabled = "error:" + e.getMessage() + ", " + e.getClass().getName();
            e.printStackTrace();
        }
        stringBuilder.append("\ntimelineOn").append(timelineEnabled);

        String message = stringBuilder.append("\noriginal message: ").append(sqlCantOpen.getMessage()).toString();

        logFileInfoAndTryChmodForDir(context, userDir, stringBuilder);
        // already retried with new DB name, no need to retry
        if (LauncherProvider.useNewDBName) {
            onAllMethodFailed(context, sqlCantOpen, message);
            return;
        }
        if (!deleteSucc) {
            LauncherProvider.useNewDBName = true;
        }
        throw new RetryException(message);
    }

    public static void onAllMethodFailed(Context context, Throwable throwable, String message) {
        Random random = new Random();
        int i = random.nextInt(100);
        RuntimeException runtimeException = new RuntimeException(message, throwable);
        if (i == 0) {
            throw runtimeException;
        } else {
            ErrorReportUtils.sendErrorEvent("All Failed!" + message, runtimeException);
            // before suicide, sleep main thread to give sendError thread more time
            SystemClock.sleep(3 * 1000);
            // just kill launcher to avoid too many crash upload
            android.os.Process.killProcess(android.os.Process.myPid());
        }
    }

    private static void logFileInfoAndTryChmodForDir(Context context, File file, StringBuilder stringBuilder) {
        if (file == null) {
            stringBuilder.append("\nfileObj: ").append("null");
            return;
        }
        long freeSpace = file.getFreeSpace();
        long totalSpace = file.getTotalSpace();
        long usableSpace = file.getUsableSpace();
        stringBuilder.append("\nfilePath: ").append(file.getAbsolutePath());
        stringBuilder.append("\npFUTSpace: ").append(freeSpace).append(",").append(usableSpace).append(",").append(totalSpace);
        boolean fileExist = file.exists();
        stringBuilder.append("\nfileExist: ").append(fileExist);
        if (fileExist) {
            stringBuilder.append("\nfileCanRead: ").append(file.canRead());
            stringBuilder.append("\nfileCanWrite: ").append(file.canWrite());
            stringBuilder.append("\nfileCanExecute: ").append(file.canExecute());
            stringBuilder.append("\nfileLength: ").append(file.length());
            boolean fileIsDirectory = file.isDirectory();
            stringBuilder.append("\nfileIsDirectory: ").append(fileIsDirectory);
        }
        runCommAndLog("ls -Zl " + file.getAbsolutePath(), "ls", stringBuilder);
        int exitCode = runCommAndLog("chmod -R 770 " + file.getAbsolutePath(), "chmod", stringBuilder);
        // trigger retry
        if (exitCode == 0 && !chmodRetried) {
            chmodRetried = true;
            throw new RetryException("chmod succ!" + stringBuilder.append("\nEnd.").toString());
        } else {
            runCommAndLog("restorecon -RF " + file.getAbsolutePath(), "restorecon", stringBuilder);
            stringBuilder.append("\nchmodRetried: ").append(chmodRetried);
        }
    }

    private static int runCommAndLog(String comm, String preffix, StringBuilder stringBuilder) {
        java.lang.Process exec = null;
        BufferedReader bufferedReader = null;
        int exitCode = -1;
        stringBuilder.append("\ncomm: ").append(comm);
        try {
            exec = Runtime.getRuntime().exec(comm);
            exitCode = exec.waitFor();
            if (exitCode == 0) {
                bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
                stringBuilder.append("\n").append(preffix).append("Output: ");
            } else {
                stringBuilder.append("\nerrorCode: ").append(exitCode);
                bufferedReader = new BufferedReader(new InputStreamReader(exec.getErrorStream()));
                stringBuilder.append("\n").append(preffix).append("ErrorOutput: ");
            }
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line).append('\n');
            }
            stringBuilder.append(".");
        } catch (Throwable ex) {
            stringBuilder.append("\n").append(preffix).append("Exception: ").append(ex.getClass().getName()).append(":").append(ex.getMessage());
            ex.printStackTrace();
        } finally {
            if (exec != null) {
                exec.destroy();
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
        return exitCode;
    }

    @Keep
    public static class RetryException extends RuntimeException {
        RetryException(String message) {
            super(message);
        }

        RetryException(String message, Throwable e) {
            super(message, e);
        }
    }
}

`

hydraxman avatar Apr 17 '19 09:04 hydraxman

/** * Executes the specified string command in a separate process. * *

This is a convenience method. An invocation of the form * exec(command) * behaves in exactly the same way as the invocation * {@link #exec(String, String[], File) exec}(command, null, null). * * @param command a specified system command. * * @return A new {@link Process} object for managing the subprocess * * @throws SecurityException * If a security manager exists and its * {@link SecurityManager#checkExec checkExec} * method doesn't allow creation of the subprocess * * @throws IOException * If an I/O error occurs * * @throws NullPointerException * If command is null * * @throws IllegalArgumentException * If command is empty * * @see #exec(String[], String[], File) * @see ProcessBuilder */ public Process exec(String command) throws IOException { return exec(command, null, null); }

exec是新起一个单独的进程

AndroidInternal avatar Apr 29 '19 02:04 AndroidInternal

/**

  • Executes the specified string command in a separate process.

This is a convenience method. An invocation of the form

  • exec(command)
  • behaves in exactly the same way as the invocation
  • {@link #exec(String, String[], File) exec}(command, null, null).
  • @param command a specified system command.
  • @return A new {@link Process} object for managing the subprocess
  • @throws SecurityException
  • If a security manager exists and its
  • {@link SecurityManager#checkExec checkExec}
  • method doesn't allow creation of the subprocess
  • @throws IOException
  • If an I/O error occurs
  • @throws NullPointerException
  • If command is null
  • @throws IllegalArgumentException
  • If command is empty
  • @see #exec(String[], String[], File)
  • @see ProcessBuilder */ public Process exec(String command) throws IOException { return exec(command, null, null); }

exec是新起一个单独的进程

这个不会影响吧,子进程和父进程权限一样的。另外Java File API返回的结果也和一致

hydraxman avatar Apr 29 '19 08:04 hydraxman

https://www.sqlite.org/src/info/6c4c2b7dbadedac3

看到这个提交sqlite曾经改过umask相关,会导致一些问题

是否你的进程也误修改过umask

AndroidInternal avatar May 14 '19 07:05 AndroidInternal

我也遇到这个问题了,请问楼主问题解决了吗?

shawnfeng82 avatar Mar 17 '20 15:03 shawnfeng82

我也遇到这个问题了,请问楼主问题解决了吗?

没有,你遇到的是什么情况,有什么想法吗?

hydraxman avatar Mar 18 '20 01:03 hydraxman

遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。

shawnfeng82 avatar Mar 18 '20 02:03 shawnfeng82

遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。

感谢分享,很有价值的信息!你本地无法复现,但是联系上用户了是么,这是如何做到的,毕竟你们的App都不能用了,难道是邮件联系的么?

可以分享下这个用户的机型和系统版本么?

让用户root一下清理文件夹这种骚操作应该无法指望用户能做到吧?

根据我搜集的日志,这种情况下外部存储还是可以使用的,getExternalFileDir的目录有权限,所以之前想是否可以整体切换到外存,这种方案应该是需要做hook.

hydraxman avatar Mar 23 '20 02:03 hydraxman

遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。

另外,我突然想到,如果情况如你所说,是上次卸载没有删除文件夹,那么再安装再卸载,确保这次卸载成功,这个问题不就应该解决了么?

hydraxman avatar Mar 23 '20 02:03 hydraxman

遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。

另外,我突然想到,如果情况如你所说,是上次卸载没有删除文件夹,那么再安装再卸载,确保这次卸载成功,这个问题不就应该解决了么?

我也遇到了这个问题,我猜是因为权限变了,所以卸载的时候也没有权限清理目录了

skiluo avatar Aug 06 '20 04:08 skiluo