Require permission for setUserInitiated().
Enforce that both the source and calling apps have the
RUN_LONG_JOBS permission granted when attempting to
schedule a user-initiated job.
Bug: 261999509
Test: atest CtsJobSchedulerTestCases:JobInfoTest
Test: atest CtsJobSchedulerTestCases:JobParametersTest
Test: atest CtsJobSchedulerTestCases:JobSchedulingTest
Change-Id: I0c7ef77be7b2e3c23ab1c790898ecb2c4bfd6bb4
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 659db9f..c9981da 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -400,6 +400,8 @@
* Returns {@code true} if the app currently holds the
* {@link android.Manifest.permission#RUN_LONG_JOBS} permission, allowing it to run long jobs.
* @hide
+ * TODO(255371817): consider exposing this to apps who could call
+ * {@link #scheduleAsPackage(JobInfo, String, int, String)}
*/
public boolean hasRunLongJobsPermission(@NonNull String packageName, @UserIdInt int userId) {
return false;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 1898e49..8652c2d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -3666,11 +3666,8 @@
return canPersist;
}
- private void validateJob(JobInfo job, int callingUid) {
- validateJob(job, callingUid, null);
- }
-
- private void validateJob(JobInfo job, int callingUid, @Nullable JobWorkItem jobWorkItem) {
+ private int validateJob(@NonNull JobInfo job, int callingUid, int sourceUserId,
+ @Nullable String sourcePkgName, @Nullable JobWorkItem jobWorkItem) {
final boolean rejectNegativeNetworkEstimates = CompatChanges.isChangeEnabled(
JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES, callingUid);
job.enforceValidity(
@@ -3690,6 +3687,37 @@
+ " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job);
}
}
+ if (job.isUserInitiated()) {
+ int sourceUid = -1;
+ if (sourceUserId != -1 && sourcePkgName != null) {
+ try {
+ sourceUid = AppGlobals.getPackageManager().getPackageUid(
+ sourcePkgName, 0, sourceUserId);
+ } catch (RemoteException ex) {
+ // Can't happen, PackageManager runs in the same process.
+ }
+ }
+ // We aim to check the permission of both the source and calling app so that apps
+ // don't attempt to bypass the permission by using other apps to do the work.
+ if (sourceUid != -1) {
+ // Check the permission of the source app.
+ final int sourceResult =
+ validateRunLongJobsPermission(sourceUid, sourcePkgName);
+ if (sourceResult != JobScheduler.RESULT_SUCCESS) {
+ return sourceResult;
+ }
+ }
+ final String callingPkgName = job.getService().getPackageName();
+ if (callingUid != sourceUid || !callingPkgName.equals(sourcePkgName)) {
+ // Source app is different from calling app. Make sure the calling app also has
+ // the permission.
+ final int callingResult =
+ validateRunLongJobsPermission(callingUid, callingPkgName);
+ if (callingResult != JobScheduler.RESULT_SUCCESS) {
+ return callingResult;
+ }
+ }
+ }
if (jobWorkItem != null) {
jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates);
if (jobWorkItem.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN
@@ -3710,6 +3738,19 @@
}
}
}
+ return JobScheduler.RESULT_SUCCESS;
+ }
+
+ private int validateRunLongJobsPermission(int uid, String packageName) {
+ final int state = getRunLongJobsPermissionState(uid, packageName);
+ if (state == PermissionChecker.PERMISSION_HARD_DENIED) {
+ throw new SecurityException(android.Manifest.permission.RUN_LONG_JOBS
+ + " required to schedule user-initiated jobs.");
+ }
+ if (state == PermissionChecker.PERMISSION_SOFT_DENIED) {
+ return JobScheduler.RESULT_FAILURE;
+ }
+ return JobScheduler.RESULT_SUCCESS;
}
// IJobScheduler implementation
@@ -3730,7 +3771,10 @@
}
}
- validateJob(job, uid);
+ final int result = validateJob(job, uid, -1, null, null);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
final long ident = Binder.clearCallingIdentity();
try {
@@ -3758,7 +3802,10 @@
throw new NullPointerException("work is null");
}
- validateJob(job, uid, work);
+ final int result = validateJob(job, uid, -1, null, work);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
final long ident = Binder.clearCallingIdentity();
try {
@@ -3789,7 +3836,10 @@
+ " not permitted to schedule jobs for other apps");
}
- validateJob(job, callerUid);
+ int result = validateJob(job, callerUid, userId, packageName, null);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
final long ident = Binder.clearCallingIdentity();
try {
@@ -4263,11 +4313,16 @@
return 0;
}
+ /** Returns true if both the appop and permission are granted. */
private boolean checkRunLongJobsPermission(int packageUid, String packageName) {
- // Returns true if both the appop and permission are granted.
+ return getRunLongJobsPermissionState(packageUid, packageName)
+ == PermissionChecker.PERMISSION_GRANTED;
+ }
+
+ private int getRunLongJobsPermissionState(int packageUid, String packageName) {
return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN,
- packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED;
+ packageUid, packageName);
}
@VisibleForTesting