1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.graphics.fonts; 18 19 import android.Manifest; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.RequiresPermission; 23 import android.content.Context; 24 import android.graphics.Typeface; 25 import android.graphics.fonts.FontFamily; 26 import android.graphics.fonts.FontManager; 27 import android.graphics.fonts.FontUpdateRequest; 28 import android.graphics.fonts.SystemFonts; 29 import android.os.Build; 30 import android.os.ParcelFileDescriptor; 31 import android.os.ResultReceiver; 32 import android.os.SharedMemory; 33 import android.os.ShellCallback; 34 import android.system.ErrnoException; 35 import android.text.FontConfig; 36 import android.util.AndroidException; 37 import android.util.ArrayMap; 38 import android.util.IndentingPrintWriter; 39 import android.util.Log; 40 import android.util.Slog; 41 42 import com.android.internal.R; 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.internal.graphics.fonts.IFontManager; 45 import com.android.internal.security.VerityUtils; 46 import com.android.internal.util.DumpUtils; 47 import com.android.internal.util.Preconditions; 48 import com.android.server.LocalServices; 49 import com.android.server.SystemService; 50 51 import java.io.File; 52 import java.io.FileDescriptor; 53 import java.io.FileInputStream; 54 import java.io.IOException; 55 import java.io.InputStream; 56 import java.io.PrintWriter; 57 import java.nio.ByteBuffer; 58 import java.nio.DirectByteBuffer; 59 import java.nio.NioUtils; 60 import java.util.Collections; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Objects; 64 65 /** A service for managing system fonts. */ 66 public final class FontManagerService extends IFontManager.Stub { 67 private static final String TAG = "FontManagerService"; 68 69 private static final String FONT_FILES_DIR = "/data/fonts/files"; 70 private static final String CONFIG_XML_FILE = "/data/fonts/config/config.xml"; 71 72 @android.annotation.EnforcePermission(android.Manifest.permission.UPDATE_FONTS) 73 @RequiresPermission(Manifest.permission.UPDATE_FONTS) 74 @Override getFontConfig()75 public FontConfig getFontConfig() { 76 super.getFontConfig_enforcePermission(); 77 78 return getSystemFontConfig(); 79 } 80 81 @RequiresPermission(Manifest.permission.UPDATE_FONTS) 82 @Override updateFontFamily(@onNull List<FontUpdateRequest> requests, int baseVersion)83 public int updateFontFamily(@NonNull List<FontUpdateRequest> requests, int baseVersion) { 84 try { 85 Preconditions.checkArgumentNonnegative(baseVersion); 86 Objects.requireNonNull(requests); 87 getContext().enforceCallingPermission(Manifest.permission.UPDATE_FONTS, 88 "UPDATE_FONTS permission required."); 89 try { 90 update(baseVersion, requests); 91 return FontManager.RESULT_SUCCESS; 92 } catch (SystemFontException e) { 93 Slog.e(TAG, "Failed to update font family", e); 94 return e.getErrorCode(); 95 } 96 } finally { 97 closeFileDescriptors(requests); 98 } 99 } 100 closeFileDescriptors(@ullable List<FontUpdateRequest> requests)101 private static void closeFileDescriptors(@Nullable List<FontUpdateRequest> requests) { 102 // Make sure we close every passed FD, even if 'requests' is constructed incorrectly and 103 // some fields are null. 104 if (requests == null) return; 105 for (FontUpdateRequest request : requests) { 106 if (request == null) continue; 107 ParcelFileDescriptor fd = request.getFd(); 108 if (fd == null) continue; 109 try { 110 fd.close(); 111 } catch (IOException e) { 112 Slog.w(TAG, "Failed to close fd", e); 113 } 114 } 115 } 116 117 /* package */ static class SystemFontException extends AndroidException { 118 private final int mErrorCode; 119 SystemFontException(@ontManager.ResultCode int errorCode, String msg, Throwable cause)120 SystemFontException(@FontManager.ResultCode int errorCode, String msg, Throwable cause) { 121 super(msg, cause); 122 mErrorCode = errorCode; 123 } 124 SystemFontException(int errorCode, String msg)125 SystemFontException(int errorCode, String msg) { 126 super(msg); 127 mErrorCode = errorCode; 128 } 129 130 @FontManager.ResultCode getErrorCode()131 int getErrorCode() { 132 return mErrorCode; 133 } 134 } 135 136 /** Class to manage FontManagerService's lifecycle. */ 137 public static final class Lifecycle extends SystemService { 138 private final FontManagerService mService; 139 Lifecycle(@onNull Context context, boolean safeMode)140 public Lifecycle(@NonNull Context context, boolean safeMode) { 141 super(context); 142 mService = new FontManagerService(context, safeMode); 143 } 144 145 @Override onStart()146 public void onStart() { 147 LocalServices.addService(FontManagerInternal.class, 148 new FontManagerInternal() { 149 @Override 150 @Nullable 151 public SharedMemory getSerializedSystemFontMap() { 152 if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) { 153 return null; 154 } 155 return mService.getCurrentFontMap(); 156 } 157 }); 158 publishBinderService(Context.FONT_SERVICE, mService); 159 } 160 } 161 162 private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil { 163 164 private final String[] mDerCertPaths; 165 FsverityUtilImpl(String[] derCertPaths)166 FsverityUtilImpl(String[] derCertPaths) { 167 mDerCertPaths = derCertPaths; 168 } 169 170 @Override isFromTrustedProvider(String fontPath, byte[] pkcs7Signature)171 public boolean isFromTrustedProvider(String fontPath, byte[] pkcs7Signature) { 172 final byte[] digest = VerityUtils.getFsverityDigest(fontPath); 173 if (digest == null) { 174 Log.w(TAG, "Failed to get fs-verity digest for " + fontPath); 175 return false; 176 } 177 for (String certPath : mDerCertPaths) { 178 try (InputStream is = new FileInputStream(certPath)) { 179 if (VerityUtils.verifyPkcs7DetachedSignature(pkcs7Signature, digest, is)) { 180 return true; 181 } 182 } catch (IOException e) { 183 Log.w(TAG, "Failed to read certificate file: " + certPath); 184 } 185 } 186 return false; 187 } 188 189 @Override setUpFsverity(String filePath)190 public void setUpFsverity(String filePath) throws IOException { 191 VerityUtils.setUpFsverity(filePath); 192 } 193 194 @Override rename(File src, File dest)195 public boolean rename(File src, File dest) { 196 // rename system call preserves fs-verity bit. 197 return src.renameTo(dest); 198 } 199 } 200 201 @NonNull 202 private final Context mContext; 203 204 private final boolean mIsSafeMode; 205 206 private final Object mUpdatableFontDirLock = new Object(); 207 208 private String mDebugCertFilePath = null; 209 210 @GuardedBy("mUpdatableFontDirLock") 211 @Nullable 212 private UpdatableFontDir mUpdatableFontDir; 213 214 // mSerializedFontMapLock can be acquired while holding mUpdatableFontDirLock. 215 // mUpdatableFontDirLock should not be newly acquired while holding mSerializedFontMapLock. 216 private final Object mSerializedFontMapLock = new Object(); 217 218 @GuardedBy("mSerializedFontMapLock") 219 @Nullable 220 private SharedMemory mSerializedFontMap = null; 221 FontManagerService(Context context, boolean safeMode)222 private FontManagerService(Context context, boolean safeMode) { 223 if (safeMode) { 224 Slog.i(TAG, "Entering safe mode. Deleting all font updates."); 225 UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE)); 226 } 227 mContext = context; 228 mIsSafeMode = safeMode; 229 initialize(); 230 } 231 232 @Nullable createUpdatableFontDir()233 private UpdatableFontDir createUpdatableFontDir() { 234 // Never read updatable font files in safe mode. 235 if (mIsSafeMode) return null; 236 // If apk verity is supported, fs-verity should be available. 237 if (!VerityUtils.isFsVeritySupported()) return null; 238 239 String[] certs = mContext.getResources().getStringArray( 240 R.array.config_fontManagerServiceCerts); 241 242 if (mDebugCertFilePath != null && Build.IS_DEBUGGABLE) { 243 String[] tmp = new String[certs.length + 1]; 244 System.arraycopy(certs, 0, tmp, 0, certs.length); 245 tmp[certs.length] = mDebugCertFilePath; 246 certs = tmp; 247 } 248 249 return new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser(), 250 new FsverityUtilImpl(certs), new File(CONFIG_XML_FILE)); 251 } 252 253 /** 254 * Add debug certificate to the cert list. This must be called only on debuggable build. 255 * 256 * @param debugCertPath a debug certificate file path 257 */ addDebugCertificate(@ullable String debugCertPath)258 public void addDebugCertificate(@Nullable String debugCertPath) { 259 mDebugCertFilePath = debugCertPath; 260 } 261 initialize()262 private void initialize() { 263 synchronized (mUpdatableFontDirLock) { 264 mUpdatableFontDir = createUpdatableFontDir(); 265 if (mUpdatableFontDir == null) { 266 setSerializedFontMap(serializeSystemServerFontMap()); 267 return; 268 } 269 mUpdatableFontDir.loadFontFileMap(); 270 updateSerializedFontMap(); 271 } 272 } 273 274 @NonNull getContext()275 public Context getContext() { 276 return mContext; 277 } 278 getCurrentFontMap()279 @Nullable /* package */ SharedMemory getCurrentFontMap() { 280 synchronized (mSerializedFontMapLock) { 281 return mSerializedFontMap; 282 } 283 } 284 update(int baseVersion, List<FontUpdateRequest> requests)285 /* package */ void update(int baseVersion, List<FontUpdateRequest> requests) 286 throws SystemFontException { 287 synchronized (mUpdatableFontDirLock) { 288 if (mUpdatableFontDir == null) { 289 throw new SystemFontException( 290 FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED, 291 "The font updater is disabled."); 292 } 293 // baseVersion == -1 only happens from shell command. This is filtered and treated as 294 // error from SystemApi call. 295 if (baseVersion != -1 && mUpdatableFontDir.getConfigVersion() != baseVersion) { 296 throw new SystemFontException( 297 FontManager.RESULT_ERROR_VERSION_MISMATCH, 298 "The base config version is older than current."); 299 } 300 mUpdatableFontDir.update(requests); 301 updateSerializedFontMap(); 302 } 303 } 304 305 /** 306 * Clears all updates and restarts FontManagerService. 307 * 308 * <p>CAUTION: this method is not safe. Existing processes may crash due to missing font files. 309 * This method is only for {@link FontManagerShellCommand}. 310 */ clearUpdates()311 /* package */ void clearUpdates() { 312 UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE)); 313 initialize(); 314 } 315 316 /** 317 * Restarts FontManagerService, removing not-the-latest font files. 318 * 319 * <p>CAUTION: this method is not safe. Existing processes may crash due to missing font files. 320 * This method is only for {@link FontManagerShellCommand}. 321 */ restart()322 /* package */ void restart() { 323 initialize(); 324 } 325 getFontFileMap()326 /* package */ Map<String, File> getFontFileMap() { 327 synchronized (mUpdatableFontDirLock) { 328 if (mUpdatableFontDir == null) { 329 return Collections.emptyMap(); 330 } 331 return mUpdatableFontDir.getPostScriptMap(); 332 } 333 } 334 335 @Override dump(@onNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args)336 public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, 337 @Nullable String[] args) { 338 if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return; 339 new FontManagerShellCommand(this).dumpAll(new IndentingPrintWriter(writer, " ")); 340 } 341 342 @Override onShellCommand(@ullable FileDescriptor in, @Nullable FileDescriptor out, @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, @NonNull ResultReceiver result)343 public void onShellCommand(@Nullable FileDescriptor in, 344 @Nullable FileDescriptor out, 345 @Nullable FileDescriptor err, 346 @NonNull String[] args, 347 @Nullable ShellCallback callback, 348 @NonNull ResultReceiver result) { 349 new FontManagerShellCommand(this).exec(this, in, out, err, args, callback, result); 350 } 351 352 /** 353 * Returns an active system font configuration. 354 */ getSystemFontConfig()355 public @NonNull FontConfig getSystemFontConfig() { 356 synchronized (mUpdatableFontDirLock) { 357 if (mUpdatableFontDir == null) { 358 return SystemFonts.getSystemPreinstalledFontConfig(); 359 } 360 return mUpdatableFontDir.getSystemFontConfig(); 361 } 362 } 363 364 /** 365 * Makes new serialized font map data and updates mSerializedFontMap. 366 */ updateSerializedFontMap()367 private void updateSerializedFontMap() { 368 SharedMemory serializedFontMap = serializeFontMap(getSystemFontConfig()); 369 if (serializedFontMap == null) { 370 // Fallback to the preloaded config. 371 serializedFontMap = serializeSystemServerFontMap(); 372 } 373 setSerializedFontMap(serializedFontMap); 374 } 375 376 @Nullable serializeFontMap(FontConfig fontConfig)377 private static SharedMemory serializeFontMap(FontConfig fontConfig) { 378 final ArrayMap<String, ByteBuffer> bufferCache = new ArrayMap<>(); 379 try { 380 final Map<String, FontFamily[]> fallback = 381 SystemFonts.buildSystemFallback(fontConfig, bufferCache); 382 final Map<String, Typeface> typefaceMap = 383 SystemFonts.buildSystemTypefaces(fontConfig, fallback); 384 return Typeface.serializeFontMap(typefaceMap); 385 } catch (IOException | ErrnoException e) { 386 Slog.w(TAG, "Failed to serialize updatable font map. " 387 + "Retrying with system image fonts.", e); 388 return null; 389 } finally { 390 // Unmap buffers promptly, as we map a lot of files and may hit mmap limit before 391 // GC collects ByteBuffers and unmaps them. 392 for (ByteBuffer buffer : bufferCache.values()) { 393 if (buffer instanceof DirectByteBuffer) { 394 NioUtils.freeDirectBuffer(buffer); 395 } 396 } 397 } 398 } 399 400 @Nullable serializeSystemServerFontMap()401 private static SharedMemory serializeSystemServerFontMap() { 402 try { 403 return Typeface.serializeFontMap(Typeface.getSystemFontMap()); 404 } catch (IOException | ErrnoException e) { 405 Slog.e(TAG, "Failed to serialize SystemServer system font map", e); 406 return null; 407 } 408 } 409 setSerializedFontMap(SharedMemory serializedFontMap)410 private void setSerializedFontMap(SharedMemory serializedFontMap) { 411 SharedMemory oldFontMap = null; 412 synchronized (mSerializedFontMapLock) { 413 oldFontMap = mSerializedFontMap; 414 mSerializedFontMap = serializedFontMap; 415 } 416 if (oldFontMap != null) { 417 oldFontMap.close(); 418 } 419 } 420 } 421