android-discuss
android-discuss copied to clipboard
各位遇到过对自己的内部存储空间没有访问权限的情况吗?
线上问题,线下无法复现: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)有关
这个日志的格式、来源是什么
ls命令没有-z参数
执行ls、chmod命令的进程是什么
这个日志的格式、来源是什么
ls命令没有-z参数
执行ls、chmod命令的进程是什么
@AndroidInternal 有-Z参数,-Z用来查看SEAndroid的文件的SEContext tag 日志是线上崩溃log发到我们服务器上的,执行的进程是我们的App,App没有root权限,就是一般的app
执行命令的应该是一个新的执行shell命令的单独的process吧,没办法直接在app所在进程执行shell命令。
还有就是,这里面的“packageName”不是真正的包名,不知道是上传上来就是这样,还是你脱敏处理了。
https://happybevis.github.io/2018/05/02/The-Magic-Selinux-Restore-Rule/
这篇文章讲到了一个大概由于用户升级导致的selinux权限变更
https://happybevis.github.io/2018/05/02/The-Magic-Selinux-Restore-Rule/
这篇文章讲到了一个大概由于用户升级导致的selinux权限变更
- 可以在app进程执行shell啊,用Runtime API
- 脱敏了
- 这篇文章我也看过了,感谢你帮忙查看。推测是文章中的原因,但是由于是线上日志,无法证明就是root cause。而且针对这种情况,作为APP进程似乎无法修复。
我们这边有几十台线上的用户手机每天报几万条这个crash: SQLiteCantOpenDatabaseException 原因就是app自己因为没有权限读不到自己内部存储的sqlite数据库,非常诡异。
收集崩溃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);
}
}
}
`
/** * 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是新起一个单独的进程
/**
- 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
isnull
- @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返回的结果也和一致
https://www.sqlite.org/src/info/6c4c2b7dbadedac3
看到这个提交sqlite曾经改过umask相关,会导致一些问题
是否你的进程也误修改过umask
我也遇到这个问题了,请问楼主问题解决了吗?
我也遇到这个问题了,请问楼主问题解决了吗?
没有,你遇到的是什么情况,有什么想法吗?
遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。
遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。
感谢分享,很有价值的信息!你本地无法复现,但是联系上用户了是么,这是如何做到的,毕竟你们的App都不能用了,难道是邮件联系的么?
可以分享下这个用户的机型和系统版本么?
让用户root一下清理文件夹这种骚操作应该无法指望用户能做到吧?
根据我搜集的日志,这种情况下外部存储还是可以使用的,getExternalFileDir的目录有权限,所以之前想是否可以整体切换到外存,这种方案应该是需要做hook.
遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。
另外,我突然想到,如果情况如你所说,是上次卸载没有删除文件夹,那么再安装再卸载,确保这次卸载成功,这个问题不就应该解决了么?
遇到的情况是一样的,就是创建数据库的时候报没有权限,然后也试着卸载应用,再重装也没用。创建其他文件也是一直没有权限。这个用户之前使用app是正常的,只是再一次卸载重装后就出现了这个问题,我怀疑是那次卸载内部存储文件夹没有被删除,再次重装,文件夹的权限就不匹配了。目前来看应用内是无法解决的,只能让这个用户root一下,清理这个文件夹了。
另外,我突然想到,如果情况如你所说,是上次卸载没有删除文件夹,那么再安装再卸载,确保这次卸载成功,这个问题不就应该解决了么?
我也遇到了这个问题,我猜是因为权限变了,所以卸载的时候也没有权限清理目录了