/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server; import static android.os.SystemUpdateManager.KEY_STATUS; import static android.os.SystemUpdateManager.STATUS_IDLE; import static android.os.SystemUpdateManager.STATUS_UNKNOWN; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; import static org.xmlpull.v1.XmlPullParser.START_TAG; import android.Manifest; import android.annotation.Nullable; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.ISystemUpdateManager; import android.os.PersistableBundle; import android.os.SystemUpdateManager; import android.provider.Settings; import android.util.AtomicFile; import android.util.Slog; import android.util.Xml; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; public class SystemUpdateManagerService extends ISystemUpdateManager.Stub { private static final String TAG = "SystemUpdateManagerService"; private static final int UID_UNKNOWN = -1; private static final String INFO_FILE = "system-update-info.xml"; private static final int INFO_FILE_VERSION = 0; private static final String TAG_INFO = "info"; private static final String KEY_VERSION = "version"; private static final String KEY_UID = "uid"; private static final String KEY_BOOT_COUNT = "boot-count"; private static final String KEY_INFO_BUNDLE = "info-bundle"; private final Context mContext; private final AtomicFile mFile; private final Object mLock = new Object(); private int mLastUid = UID_UNKNOWN; private int mLastStatus = STATUS_UNKNOWN; public SystemUpdateManagerService(Context context) { mContext = context; mFile = new AtomicFile(new File(Environment.getDataSystemDirectory(), INFO_FILE)); // Populate mLastUid and mLastStatus. synchronized (mLock) { loadSystemUpdateInfoLocked(); } } @Override public void updateSystemUpdateInfo(PersistableBundle infoBundle) { mContext.enforceCallingOrSelfPermission(Manifest.permission.RECOVERY, TAG); int status = infoBundle.getInt(KEY_STATUS, STATUS_UNKNOWN); if (status == STATUS_UNKNOWN) { Slog.w(TAG, "Invalid status info. Ignored"); return; } // There could be multiple updater apps running on a device. But only one at most should // be active (i.e. with a pending update), with the rest reporting idle status. We will // only accept the reported status if any of the following conditions holds: // a) none has been reported before; // b) the current on-file status was last reported by the same caller; // c) an active update is being reported. int uid = Binder.getCallingUid(); if (mLastUid == UID_UNKNOWN || mLastUid == uid || status != STATUS_IDLE) { synchronized (mLock) { saveSystemUpdateInfoLocked(infoBundle, uid); } } else { Slog.i(TAG, "Inactive updater reporting IDLE status. Ignored"); } } @Override public Bundle retrieveSystemUpdateInfo() { if (mContext.checkCallingOrSelfPermission(Manifest.permission.READ_SYSTEM_UPDATE_INFO) == PackageManager.PERMISSION_DENIED && mContext.checkCallingOrSelfPermission(Manifest.permission.RECOVERY) == PackageManager.PERMISSION_DENIED) { throw new SecurityException("Can't read system update info. Requiring " + "READ_SYSTEM_UPDATE_INFO or RECOVERY permission."); } synchronized (mLock) { return loadSystemUpdateInfoLocked(); } } // Reads and validates the info file. Returns the loaded info bundle on success; or a default // info bundle with UNKNOWN status. private Bundle loadSystemUpdateInfoLocked() { PersistableBundle loadedBundle = null; try (FileInputStream fis = mFile.openRead()) { XmlPullParser parser = Xml.newPullParser(); parser.setInput(fis, StandardCharsets.UTF_8.name()); loadedBundle = readInfoFileLocked(parser); } catch (FileNotFoundException e) { Slog.i(TAG, "No existing info file " + mFile.getBaseFile()); } catch (XmlPullParserException e) { Slog.e(TAG, "Failed to parse the info file:", e); } catch (IOException e) { Slog.e(TAG, "Failed to read the info file:", e); } // Validate the loaded bundle. if (loadedBundle == null) { return removeInfoFileAndGetDefaultInfoBundleLocked(); } int version = loadedBundle.getInt(KEY_VERSION, -1); if (version == -1) { Slog.w(TAG, "Invalid info file (invalid version). Ignored"); return removeInfoFileAndGetDefaultInfoBundleLocked(); } int lastUid = loadedBundle.getInt(KEY_UID, -1); if (lastUid == -1) { Slog.w(TAG, "Invalid info file (invalid UID). Ignored"); return removeInfoFileAndGetDefaultInfoBundleLocked(); } int lastBootCount = loadedBundle.getInt(KEY_BOOT_COUNT, -1); if (lastBootCount == -1 || lastBootCount != getBootCount()) { Slog.w(TAG, "Outdated info file. Ignored"); return removeInfoFileAndGetDefaultInfoBundleLocked(); } PersistableBundle infoBundle = loadedBundle.getPersistableBundle(KEY_INFO_BUNDLE); if (infoBundle == null) { Slog.w(TAG, "Invalid info file (missing info). Ignored"); return removeInfoFileAndGetDefaultInfoBundleLocked(); } int lastStatus = infoBundle.getInt(KEY_STATUS, STATUS_UNKNOWN); if (lastStatus == STATUS_UNKNOWN) { Slog.w(TAG, "Invalid info file (invalid status). Ignored"); return removeInfoFileAndGetDefaultInfoBundleLocked(); } // Everything looks good upon reaching this point. mLastStatus = lastStatus; mLastUid = lastUid; return new Bundle(infoBundle); } private void saveSystemUpdateInfoLocked(PersistableBundle infoBundle, int uid) { // Wrap the incoming bundle with extra info (e.g. version, uid, boot count). We use nested // PersistableBundle to avoid manually parsing XML attributes when loading the info back. PersistableBundle outBundle = new PersistableBundle(); outBundle.putPersistableBundle(KEY_INFO_BUNDLE, infoBundle); outBundle.putInt(KEY_VERSION, INFO_FILE_VERSION); outBundle.putInt(KEY_UID, uid); outBundle.putInt(KEY_BOOT_COUNT, getBootCount()); // Only update the info on success. if (writeInfoFileLocked(outBundle)) { mLastUid = uid; mLastStatus = infoBundle.getInt(KEY_STATUS); } } // Performs I/O work only, without validating the loaded info. @Nullable private PersistableBundle readInfoFileLocked(XmlPullParser parser) throws XmlPullParserException, IOException { int type; while ((type = parser.next()) != END_DOCUMENT) { if (type == START_TAG && TAG_INFO.equals(parser.getName())) { return PersistableBundle.restoreFromXml(parser); } } return null; } private boolean writeInfoFileLocked(PersistableBundle outBundle) { FileOutputStream fos = null; try { fos = mFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(fos, StandardCharsets.UTF_8.name()); out.startDocument(null, true); out.startTag(null, TAG_INFO); outBundle.saveToXml(out); out.endTag(null, TAG_INFO); out.endDocument(); mFile.finishWrite(fos); return true; } catch (IOException | XmlPullParserException e) { Slog.e(TAG, "Failed to save the info file:", e); if (fos != null) { mFile.failWrite(fos); } } return false; } private Bundle removeInfoFileAndGetDefaultInfoBundleLocked() { if (mFile.exists()) { Slog.i(TAG, "Removing info file"); mFile.delete(); } mLastStatus = STATUS_UNKNOWN; mLastUid = UID_UNKNOWN; Bundle infoBundle = new Bundle(); infoBundle.putInt(KEY_STATUS, STATUS_UNKNOWN); return infoBundle; } private int getBootCount() { return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.BOOT_COUNT, 0); } }