Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
hydraxman opened this issue Apr 15, 2019 · 16 comments

Comments

@hydraxman
Copy link

线上问题,线下无法复现: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)有关

@AndroidInternal
Copy link

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

ls命令没有-z参数

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

@hydraxman
Copy link
Author

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

ls命令没有-z参数

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

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

@AndroidInternal
Copy link

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

@AndroidInternal
Copy link

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

@AndroidInternal
Copy link

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

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

@hydraxman
Copy link
Author

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
Copy link
Author

hydraxman commented Apr 17, 2019

收集崩溃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);
        }
    }
}

`

@AndroidInternal
Copy link

/**
* 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是新起一个单独的进程

@hydraxman
Copy link
Author

/**

  • 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返回的结果也和一致

@AndroidInternal
Copy link

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

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

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

@shawnfeng82
Copy link

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

@hydraxman
Copy link
Author

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

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

@shawnfeng82
Copy link

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

@hydraxman
Copy link
Author

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

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

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

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

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

@hydraxman
Copy link
Author

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

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

@skiluo
Copy link

skiluo commented Aug 6, 2020

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants