Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a40f59c7 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Require network permission for networked jobs." into udc-dev

parents 5308553f 056e4e77
Loading
Loading
Loading
Loading
+11 −0
Original line number Original line Diff line number Diff line
@@ -1382,6 +1382,12 @@ public class JobInfo implements Parcelable {
         * Calling this method will override any requirements previously defined
         * Calling this method will override any requirements previously defined
         * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only
         * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only
         * want to call one of these methods.
         * want to call one of these methods.
         *
         * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
         * an app must hold the {@link android.Manifest.permission#INTERNET} and
         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to
         * schedule a job that requires a network.
         *
         * <p class="note">
         * <p class="note">
         * When your job executes in
         * When your job executes in
         * {@link JobService#onStartJob(JobParameters)}, be sure to use the
         * {@link JobService#onStartJob(JobParameters)}, be sure to use the
@@ -1438,6 +1444,11 @@ public class JobInfo implements Parcelable {
         * otherwise you'll use the default network which may not meet this
         * otherwise you'll use the default network which may not meet this
         * constraint.
         * constraint.
         *
         *
         * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
         * an app must hold the {@link android.Manifest.permission#INTERNET} and
         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to
         * schedule a job that requires a network.
         *
         * @param networkRequest The detailed description of the kind of network
         * @param networkRequest The detailed description of the kind of network
         *            this job requires, or {@code null} if no specific kind of
         *            this job requires, or {@code null} if no specific kind of
         *            network is required. Defining a {@link NetworkSpecifier}
         *            network is required. Defining a {@link NetworkSpecifier}
+4 −0
Original line number Original line Diff line number Diff line
@@ -481,6 +481,10 @@ public class JobParameters implements Parcelable {
     * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over
     * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over
     * a metered network when there is a surplus of metered data available.
     * a metered network when there is a surplus of metered data available.
     *
     *
     * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
     * this will return {@code null} if the app does not hold the permissions specified in
     * {@link JobInfo.Builder#setRequiredNetwork(NetworkRequest)}.
     *
     * @return the network that should be used to perform any network requests
     * @return the network that should be used to perform any network requests
     *         for this job, or {@code null} if this job didn't set any required
     *         for this job, or {@code null} if this job didn't set any required
     *         network type or if the job executed when there was no available network to use.
     *         network type or if the job executed when there was no available network to use.
+85 −30
Original line number Original line Diff line number Diff line
@@ -23,6 +23,7 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;


import android.Manifest;
import android.annotation.EnforcePermission;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
@@ -190,6 +191,14 @@ public class JobSchedulerService extends com.android.server.SystemService
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L;
    private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L;


    /**
     * Require the app to have the INTERNET and ACCESS_NETWORK_STATE permissions when scheduling
     * a job with a connectivity constraint.
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    static final long REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS = 271850009L;

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public static Clock sSystemClock = Clock.systemUTC();
    public static Clock sSystemClock = Clock.systemUTC();


@@ -299,6 +308,14 @@ public class JobSchedulerService extends com.android.server.SystemService
    private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers =
    private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers =
            new RemoteCallbackList<>();
            new RemoteCallbackList<>();


    /**
     * Cache of grant status of permissions, keyed by UID->PID->permission name. A missing value
     * means the state has not been queried.
     */
    @GuardedBy("mPermissionCache")
    private final SparseArray<SparseArrayMap<String, Boolean>> mPermissionCache =
            new SparseArray<>();

    private final CountQuotaTracker mQuotaTracker;
    private final CountQuotaTracker mQuotaTracker;
    private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
    private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
    private static final String QUOTA_TRACKER_SCHEDULE_LOGGED =
    private static final String QUOTA_TRACKER_SCHEDULE_LOGGED =
@@ -1042,6 +1059,10 @@ public class JobSchedulerService extends com.android.server.SystemService
            final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
            final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);


            if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
            if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
                synchronized (mPermissionCache) {
                    // Something changed. Better clear the cached permission set.
                    mPermissionCache.remove(pkgUid);
                }
                // Purge the app's jobs if the whole package was just disabled.  When this is
                // Purge the app's jobs if the whole package was just disabled.  When this is
                // the case the component name will be a bare package name.
                // the case the component name will be a bare package name.
                if (pkgName != null && pkgUid != -1) {
                if (pkgName != null && pkgUid != -1) {
@@ -1106,17 +1127,19 @@ public class JobSchedulerService extends com.android.server.SystemService
                    Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid);
                    Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid);
                }
                }
            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
                synchronized (mPermissionCache) {
                    // Something changed. Better clear the cached permission set.
                    mPermissionCache.remove(pkgUid);
                }
                if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                    final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
                    synchronized (mLock) {
                    synchronized (mLock) {
                        mUidToPackageCache.remove(uid);
                        mUidToPackageCache.remove(pkgUid);
                    }
                } else {
                    synchronized (mJobSchedulerStub.mPersistCache) {
                        mJobSchedulerStub.mPersistCache.remove(pkgUid);
                    }
                    }
                }
                }
            } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
            } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
                synchronized (mPermissionCache) {
                    mPermissionCache.remove(pkgUid);
                }
                if (DEBUG) {
                if (DEBUG) {
                    Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")");
                    Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")");
                }
                }
@@ -1155,6 +1178,14 @@ public class JobSchedulerService extends com.android.server.SystemService
                    }
                    }
                }
                }
                mConcurrencyManager.onUserRemoved(userId);
                mConcurrencyManager.onUserRemoved(userId);
                synchronized (mPermissionCache) {
                    for (int u = mPermissionCache.size() - 1; u >= 0; --u) {
                        final int uid = mPermissionCache.keyAt(u);
                        if (userId == UserHandle.getUserId(uid)) {
                            mPermissionCache.removeAt(u);
                        }
                    }
                }
            } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
            } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
                // Has this package scheduled any jobs, such that we will take action
                // Has this package scheduled any jobs, such that we will take action
                // if it were to be force-stopped?
                // if it were to be force-stopped?
@@ -3748,18 +3779,38 @@ public class JobSchedulerService extends com.android.server.SystemService
    }
    }


    /**
    /**
     * Binder stub trampoline implementation
     * Returns whether the app has the permission granted.
     * This currently only works for normal permissions and <b>DOES NOT</b> work for runtime
     * permissions.
     * TODO: handle runtime permissions
     */
     */
    final class JobSchedulerStub extends IJobScheduler.Stub {
    private boolean hasPermission(int uid, int pid, @NonNull String permission) {
        synchronized (mPermissionCache) {
            SparseArrayMap<String, Boolean> pidPermissions = mPermissionCache.get(uid);
            if (pidPermissions == null) {
                pidPermissions = new SparseArrayMap<>();
                mPermissionCache.put(uid, pidPermissions);
            }
            final Boolean cached = pidPermissions.get(pid, permission);
            if (cached != null) {
                return cached;
            }

            final int result = getContext().checkPermission(permission, pid, uid);
            final boolean permissionGranted = (result == PackageManager.PERMISSION_GRANTED);
            pidPermissions.add(pid, permission, permissionGranted);
            return permissionGranted;
        }
    }

    /**
    /**
         * Cache determination of whether a given app can persist jobs
     * Binder stub trampoline implementation
         * key is uid of the calling app; value is undetermined/true/false
     */
     */
        private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>();
    final class JobSchedulerStub extends IJobScheduler.Stub {

        // Enforce that only the app itself (or shared uid participant) can schedule a
        // Enforce that only the app itself (or shared uid participant) can schedule a
        // job that runs one of the app's services, as well as verifying that the
        // job that runs one of the app's services, as well as verifying that the
        // named service properly requires the BIND_JOB_SERVICE permission
        // named service properly requires the BIND_JOB_SERVICE permission
        // TODO(141645789): merge enforceValidJobRequest() with validateJob()
        private void enforceValidJobRequest(int uid, int pid, JobInfo job) {
        private void enforceValidJobRequest(int uid, int pid, JobInfo job) {
            final PackageManager pm = getContext()
            final PackageManager pm = getContext()
                    .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0)
                    .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0)
@@ -3784,31 +3835,33 @@ public class JobSchedulerService extends com.android.server.SystemService
                throw new IllegalArgumentException(
                throw new IllegalArgumentException(
                        "Tried to schedule job for non-existent component: " + service);
                        "Tried to schedule job for non-existent component: " + service);
            }
            }
            // If we get this far we're good to go; all we need to do now is check
            // whether the app is allowed to persist its scheduled work.
            if (job.isPersisted() && !canPersistJobs(pid, uid)) {
            if (job.isPersisted() && !canPersistJobs(pid, uid)) {
                throw new IllegalArgumentException("Requested job cannot be persisted without"
                throw new IllegalArgumentException("Requested job cannot be persisted without"
                        + " holding android.permission.RECEIVE_BOOT_COMPLETED permission");
                        + " holding android.permission.RECEIVE_BOOT_COMPLETED permission");
            }
            }
            if (job.getRequiredNetwork() != null
                    && CompatChanges.isChangeEnabled(
                            REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) {
                // All networking, including with the local network and even local to the device,
                // requires the INTERNET permission.
                if (!hasPermission(uid, pid, Manifest.permission.INTERNET)) {
                    throw new SecurityException(Manifest.permission.INTERNET
                            + " required for jobs with a connectivity constraint");
                }
                if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) {
                    throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE
                            + " required for jobs with a connectivity constraint");
                }
            }
        }
        }


        private boolean canPersistJobs(int pid, int uid) {
        private boolean canPersistJobs(int pid, int uid) {
            // If we get this far we're good to go; all we need to do now is check
            // whether the app is allowed to persist its scheduled work.
            final boolean canPersist;
            synchronized (mPersistCache) {
                Boolean cached = mPersistCache.get(uid);
                if (cached != null) {
                    canPersist = cached.booleanValue();
                } else {
            // Persisting jobs is tantamount to running at boot, so we permit
            // Persisting jobs is tantamount to running at boot, so we permit
            // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
            // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
            // permission
            // permission
                    int result = getContext().checkPermission(
            return hasPermission(uid, pid, Manifest.permission.RECEIVE_BOOT_COMPLETED);
                            android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid);
                    canPersist = (result == PackageManager.PERMISSION_GRANTED);
                    mPersistCache.put(uid, canPersist);
                }
            }
            return canPersist;
        }
        }


        private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid,
        private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid,
@@ -4027,6 +4080,8 @@ public class JobSchedulerService extends com.android.server.SystemService
                        + " not permitted to schedule jobs for other apps");
                        + " not permitted to schedule jobs for other apps");
            }
            }


            enforceValidJobRequest(callerUid, callerPid, job);

            int result = validateJob(job, callerUid, callerPid, userId, packageName, null);
            int result = validateJob(job, callerUid, callerPid, userId, packageName, null);
            if (result != JobScheduler.RESULT_SUCCESS) {
            if (result != JobScheduler.RESULT_SUCCESS) {
                return result;
                return result;
+44 −1
Original line number Original line Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.app.job.JobInfo.getPriorityString;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;


import android.Manifest;
import android.annotation.BytesLong;
import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
@@ -39,6 +40,7 @@ import android.compat.annotation.EnabledAfter;
import android.content.ComponentName;
import android.content.ComponentName;
import android.content.Context;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.content.PermissionChecker;
import android.content.ServiceConnection;
import android.content.ServiceConnection;
import android.net.Network;
import android.net.Network;
import android.net.Uri;
import android.net.Uri;
@@ -339,12 +341,13 @@ public final class JobServiceContext implements ServiceConnection {
                job.changedAuthorities.toArray(triggeredAuthorities);
                job.changedAuthorities.toArray(triggeredAuthorities);
            }
            }
            final JobInfo ji = job.getJob();
            final JobInfo ji = job.getJob();
            final Network passedNetwork = canGetNetworkInformation(job) ? job.network : null;
            mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(),
            mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(),
                    ji.getExtras(),
                    ji.getExtras(),
                    ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
                    ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
                    isDeadlineExpired, job.shouldTreatAsExpeditedJob(),
                    isDeadlineExpired, job.shouldTreatAsExpeditedJob(),
                    job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities,
                    job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities,
                    job.network);
                    passedNetwork);
            mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
            mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
            mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job);
            mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job);
            mMaxExecutionTimeMillis =
            mMaxExecutionTimeMillis =
@@ -504,6 +507,37 @@ public final class JobServiceContext implements ServiceConnection {
        }
        }
    }
    }


    private boolean canGetNetworkInformation(@NonNull JobStatus job) {
        if (job.getJob().getRequiredNetwork() == null) {
            // The job never had a network constraint, so we're not going to give it a network
            // object. Add this check as an early return to avoid wasting cycles doing permission
            // checks for this job.
            return false;
        }
        // The calling app is doing the work, so use its UID, not the source UID.
        final int uid = job.getUid();
        if (CompatChanges.isChangeEnabled(
                JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) {
            final String pkgName = job.getServiceComponent().getPackageName();
            if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.INTERNET)) {
                return false;
            }
            if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) {
                return false;
            }
        }

        return true;
    }

    private boolean hasPermissionForDelivery(int uid, @NonNull String pkgName,
            @NonNull String permission) {
        final int result = PermissionChecker.checkPermissionForDataDelivery(mContext, permission,
                PermissionChecker.PID_UNKNOWN, uid, pkgName, /* attributionTag */ null,
                "network info via JS");
        return result == PermissionChecker.PERMISSION_GRANTED;
    }

    @EconomicPolicy.AppAction
    @EconomicPolicy.AppAction
    private static int getStartActionId(@NonNull JobStatus job) {
    private static int getStartActionId(@NonNull JobStatus job) {
        switch (job.getEffectivePriority()) {
        switch (job.getEffectivePriority()) {
@@ -603,6 +637,15 @@ public final class JobServiceContext implements ServiceConnection {
    }
    }


    void informOfNetworkChangeLocked(Network newNetwork) {
    void informOfNetworkChangeLocked(Network newNetwork) {
        if (newNetwork != null && mRunningJob != null && !canGetNetworkInformation(mRunningJob)) {
            // The app can't get network information, so there's no point informing it of network
            // changes. This case may happen if an app had scheduled network job and then
            // started targeting U+ without requesting the required network permissions.
            if (DEBUG) {
                Slog.d(TAG, "Skipping network change call because of missing permissions");
            }
            return;
        }
        if (mVerb != VERB_EXECUTING) {
        if (mVerb != VERB_EXECUTING) {
            Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob);
            Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob);
            if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) {
            if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) {