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.SystemServerInitThreadPool; 50 import com.android.server.SystemService; 51 import com.android.text.flags.Flags; 52 53 import java.io.File; 54 import java.io.FileDescriptor; 55 import java.io.FileInputStream; 56 import java.io.IOException; 57 import java.io.InputStream; 58 import java.io.PrintWriter; 59 import java.nio.ByteBuffer; 60 import java.nio.DirectByteBuffer; 61 import java.nio.NioUtils; 62 import java.util.Collections; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Objects; 66 import java.util.concurrent.CompletableFuture; 67 68 /** A service for managing system fonts. */ 69 public final class FontManagerService extends IFontManager.Stub { 70 private static final String TAG = "FontManagerService"; 71 72 private static final String FONT_FILES_DIR = "/data/fonts/files"; 73 private static final String CONFIG_XML_FILE = "/data/fonts/config/config.xml"; 74 75 @android.annotation.EnforcePermission(android.Manifest.permission.UPDATE_FONTS) 76 @RequiresPermission(Manifest.permission.UPDATE_FONTS) 77 @Override getFontConfig()78 public FontConfig getFontConfig() { 79 super.getFontConfig_enforcePermission(); 80 81 return getSystemFontConfig(); 82 } 83 84 @RequiresPermission(Manifest.permission.UPDATE_FONTS) 85 @Override updateFontFamily(@onNull List<FontUpdateRequest> requests, int baseVersion)86 public int updateFontFamily(@NonNull List<FontUpdateRequest> requests, int baseVersion) { 87 try { 88 Preconditions.checkArgumentNonnegative(baseVersion); 89 Objects.requireNonNull(requests); 90 getContext().enforceCallingPermission(Manifest.permission.UPDATE_FONTS, 91 "UPDATE_FONTS permission required."); 92 try { 93 update(baseVersion, requests); 94 return FontManager.RESULT_SUCCESS; 95 } catch (SystemFontException e) { 96 Slog.e(TAG, "Failed to update font family", e); 97 return e.getErrorCode(); 98 } 99 } finally { 100 closeFileDescriptors(requests); 101 } 102 } 103 closeFileDescriptors(@ullable List<FontUpdateRequest> requests)104 private static void closeFileDescriptors(@Nullable List<FontUpdateRequest> requests) { 105 // Make sure we close every passed FD, even if 'requests' is constructed incorrectly and 106 // some fields are null. 107 if (requests == null) return; 108 for (FontUpdateRequest request : requests) { 109 if (request == null) continue; 110 ParcelFileDescriptor fd = request.getFd(); 111 if (fd == null) continue; 112 try { 113 fd.close(); 114 } catch (IOException e) { 115 Slog.w(TAG, "Failed to close fd", e); 116 } 117 } 118 } 119 120 /* package */ static class SystemFontException extends AndroidException { 121 private final int mErrorCode; 122 SystemFontException(@ontManager.ResultCode int errorCode, String msg, Throwable cause)123 SystemFontException(@FontManager.ResultCode int errorCode, String msg, Throwable cause) { 124 super(msg, cause); 125 mErrorCode = errorCode; 126 } 127 SystemFontException(int errorCode, String msg)128 SystemFontException(int errorCode, String msg) { 129 super(msg); 130 mErrorCode = errorCode; 131 } 132 133 @FontManager.ResultCode getErrorCode()134 int getErrorCode() { 135 return mErrorCode; 136 } 137 } 138 139 /** Class to manage FontManagerService's lifecycle. */ 140 public static final class Lifecycle extends SystemService { 141 private final FontManagerService mService; 142 private final CompletableFuture<Void> mServiceStarted = new CompletableFuture<>(); 143 Lifecycle(@onNull Context context, boolean safeMode)144 public Lifecycle(@NonNull Context context, boolean safeMode) { 145 super(context); 146 mService = new FontManagerService(context, safeMode, mServiceStarted); 147 } 148 149 @Override onStart()150 public void onStart() { 151 LocalServices.addService(FontManagerInternal.class, 152 new FontManagerInternal() { 153 @Override 154 @Nullable 155 public SharedMemory getSerializedSystemFontMap() { 156 if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) { 157 return null; 158 } 159 mServiceStarted.join(); 160 return mService.getCurrentFontMap(); 161 } 162 }); 163 publishBinderService(Context.FONT_SERVICE, mService); 164 } 165 166 @Override onBootPhase(int phase)167 public void onBootPhase(int phase) { 168 final int latestFontLoadBootPhase = 169 (Flags.completeFontLoadInSystemServicesReady()) 170 // Complete font load in the phase before PHASE_SYSTEM_SERVICES_READY 171 ? SystemService.PHASE_LOCK_SETTINGS_READY 172 : SystemService.PHASE_ACTIVITY_MANAGER_READY; 173 if (phase == latestFontLoadBootPhase) { 174 // Wait for FontManagerService to start since it will be needed after this point. 175 mServiceStarted.join(); 176 } 177 } 178 } 179 180 private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil { 181 182 private final String[] mDerCertPaths; 183 FsverityUtilImpl(String[] derCertPaths)184 FsverityUtilImpl(String[] derCertPaths) { 185 mDerCertPaths = derCertPaths; 186 } 187 188 @Override isFromTrustedProvider(String fontPath, byte[] pkcs7Signature)189 public boolean isFromTrustedProvider(String fontPath, byte[] pkcs7Signature) { 190 final byte[] digest = VerityUtils.getFsverityDigest(fontPath); 191 if (digest == null) { 192 Log.w(TAG, "Failed to get fs-verity digest for " + fontPath); 193 return false; 194 } 195 for (String certPath : mDerCertPaths) { 196 try (InputStream is = new FileInputStream(certPath)) { 197 if (VerityUtils.verifyPkcs7DetachedSignature(pkcs7Signature, digest, is)) { 198 return true; 199 } 200 } catch (IOException e) { 201 Log.w(TAG, "Failed to read certificate file: " + certPath); 202 } 203 } 204 return false; 205 } 206 207 @Override setUpFsverity(String filePath)208 public void setUpFsverity(String filePath) throws IOException { 209 VerityUtils.setUpFsverity(filePath); 210 } 211 212 @Override rename(File src, File dest)213 public boolean rename(File src, File dest) { 214 // rename system call preserves fs-verity bit. 215 return src.renameTo(dest); 216 } 217 } 218 219 @NonNull 220 private final Context mContext; 221 222 private final boolean mIsSafeMode; 223 224 private final Object mUpdatableFontDirLock = new Object(); 225 226 private String mDebugCertFilePath = null; 227 228 @GuardedBy("mUpdatableFontDirLock") 229 @Nullable 230 private UpdatableFontDir mUpdatableFontDir; 231 232 // mSerializedFontMapLock can be acquired while holding mUpdatableFontDirLock. 233 // mUpdatableFontDirLock should not be newly acquired while holding mSerializedFontMapLock. 234 private final Object mSerializedFontMapLock = new Object(); 235 236 @GuardedBy("mSerializedFontMapLock") 237 @Nullable 238 private SharedMemory mSerializedFontMap = null; 239 FontManagerService( Context context, boolean safeMode, CompletableFuture<Void> serviceStarted)240 private FontManagerService( 241 Context context, boolean safeMode, CompletableFuture<Void> serviceStarted) { 242 if (safeMode) { 243 Slog.i(TAG, "Entering safe mode. Deleting all font updates."); 244 UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE)); 245 } 246 mContext = context; 247 mIsSafeMode = safeMode; 248 249 if (Flags.useOptimizedBoottimeFontLoading()) { 250 Slog.i(TAG, "Using optimized boot-time font loading."); 251 SystemServerInitThreadPool.submit(() -> { 252 initialize(); 253 254 // Set system font map only if there is updatable font directory. 255 // If there is no updatable font directory, `initialize` will have already loaded 256 // the system font map, so there's no need to set the system font map again here. 257 synchronized (mUpdatableFontDirLock) { 258 if (mUpdatableFontDir != null) { 259 setSystemFontMap(); 260 } 261 } 262 serviceStarted.complete(null); 263 }, "FontManagerService_create"); 264 } else { 265 Slog.i(TAG, "Not using optimized boot-time font loading."); 266 initialize(); 267 setSystemFontMap(); 268 serviceStarted.complete(null); 269 } 270 } 271 setSystemFontMap()272 private void setSystemFontMap() { 273 try { 274 Typeface.setSystemFontMap(getCurrentFontMap()); 275 } catch (IOException | ErrnoException e) { 276 Slog.w(TAG, "Failed to set system font map of system_server"); 277 } 278 } 279 280 @Nullable createUpdatableFontDir()281 private UpdatableFontDir createUpdatableFontDir() { 282 // Never read updatable font files in safe mode. 283 if (mIsSafeMode) return null; 284 // If apk verity is supported, fs-verity should be available. 285 if (!VerityUtils.isFsVeritySupported()) return null; 286 287 String[] certs = mContext.getResources().getStringArray( 288 R.array.config_fontManagerServiceCerts); 289 290 if (mDebugCertFilePath != null && Build.IS_DEBUGGABLE) { 291 String[] tmp = new String[certs.length + 1]; 292 System.arraycopy(certs, 0, tmp, 0, certs.length); 293 tmp[certs.length] = mDebugCertFilePath; 294 certs = tmp; 295 } 296 297 return new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser(), 298 new FsverityUtilImpl(certs), new File(CONFIG_XML_FILE)); 299 } 300 301 /** 302 * Add debug certificate to the cert list. This must be called only on debuggable build. 303 * 304 * @param debugCertPath a debug certificate file path 305 */ addDebugCertificate(@ullable String debugCertPath)306 public void addDebugCertificate(@Nullable String debugCertPath) { 307 mDebugCertFilePath = debugCertPath; 308 } 309 initialize()310 private void initialize() { 311 synchronized (mUpdatableFontDirLock) { 312 mUpdatableFontDir = createUpdatableFontDir(); 313 if (mUpdatableFontDir == null) { 314 if (Flags.useOptimizedBoottimeFontLoading()) { 315 // If fs-verity is not supported, load preinstalled system font map and use it 316 // for all apps. 317 Typeface.loadPreinstalledSystemFontMap(); 318 } 319 setSerializedFontMap(serializeSystemServerFontMap()); 320 return; 321 } 322 mUpdatableFontDir.loadFontFileMap(); 323 updateSerializedFontMap(); 324 } 325 } 326 327 @NonNull getContext()328 public Context getContext() { 329 return mContext; 330 } 331 getCurrentFontMap()332 @Nullable /* package */ SharedMemory getCurrentFontMap() { 333 synchronized (mSerializedFontMapLock) { 334 return mSerializedFontMap; 335 } 336 } 337 update(int baseVersion, List<FontUpdateRequest> requests)338 /* package */ void update(int baseVersion, List<FontUpdateRequest> requests) 339 throws SystemFontException { 340 synchronized (mUpdatableFontDirLock) { 341 if (mUpdatableFontDir == null) { 342 throw new SystemFontException( 343 FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED, 344 "The font updater is disabled."); 345 } 346 // baseVersion == -1 only happens from shell command. This is filtered and treated as 347 // error from SystemApi call. 348 if (baseVersion != -1 && mUpdatableFontDir.getConfigVersion() != baseVersion) { 349 throw new SystemFontException( 350 FontManager.RESULT_ERROR_VERSION_MISMATCH, 351 "The base config version is older than current."); 352 } 353 mUpdatableFontDir.update(requests); 354 updateSerializedFontMap(); 355 } 356 } 357 358 /** 359 * Clears all updates and restarts FontManagerService. 360 * 361 * <p>CAUTION: this method is not safe. Existing processes may crash due to missing font files. 362 * This method is only for {@link FontManagerShellCommand}. 363 */ clearUpdates()364 /* package */ void clearUpdates() { 365 UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE)); 366 initialize(); 367 } 368 369 /** 370 * Restarts FontManagerService, removing not-the-latest font files. 371 * 372 * <p>CAUTION: this method is not safe. Existing processes may crash due to missing font files. 373 * This method is only for {@link FontManagerShellCommand}. 374 */ restart()375 /* package */ void restart() { 376 initialize(); 377 } 378 getFontFileMap()379 /* package */ Map<String, File> getFontFileMap() { 380 synchronized (mUpdatableFontDirLock) { 381 if (mUpdatableFontDir == null) { 382 return Collections.emptyMap(); 383 } 384 return mUpdatableFontDir.getPostScriptMap(); 385 } 386 } 387 388 @Override dump(@onNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args)389 public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, 390 @Nullable String[] args) { 391 if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return; 392 new FontManagerShellCommand(this).dumpAll(new IndentingPrintWriter(writer, " ")); 393 } 394 395 @Override onShellCommand(@ullable FileDescriptor in, @Nullable FileDescriptor out, @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, @NonNull ResultReceiver result)396 public void onShellCommand(@Nullable FileDescriptor in, 397 @Nullable FileDescriptor out, 398 @Nullable FileDescriptor err, 399 @NonNull String[] args, 400 @Nullable ShellCallback callback, 401 @NonNull ResultReceiver result) { 402 new FontManagerShellCommand(this).exec(this, in, out, err, args, callback, result); 403 } 404 405 /** 406 * Returns an active system font configuration. 407 */ getSystemFontConfig()408 public @NonNull FontConfig getSystemFontConfig() { 409 synchronized (mUpdatableFontDirLock) { 410 if (mUpdatableFontDir == null) { 411 return SystemFonts.getSystemPreinstalledFontConfig(); 412 } 413 return mUpdatableFontDir.getSystemFontConfig(); 414 } 415 } 416 417 /** 418 * Makes new serialized font map data and updates mSerializedFontMap. 419 */ updateSerializedFontMap()420 private void updateSerializedFontMap() { 421 SharedMemory serializedFontMap = serializeFontMap(getSystemFontConfig()); 422 if (serializedFontMap == null) { 423 // Fallback to the preloaded config. 424 serializedFontMap = serializeSystemServerFontMap(); 425 } 426 setSerializedFontMap(serializedFontMap); 427 } 428 429 @Nullable serializeFontMap(FontConfig fontConfig)430 private static SharedMemory serializeFontMap(FontConfig fontConfig) { 431 final ArrayMap<String, ByteBuffer> bufferCache = new ArrayMap<>(); 432 try { 433 final Map<String, FontFamily[]> fallback = 434 SystemFonts.buildSystemFallback(fontConfig, bufferCache); 435 final Map<String, Typeface> typefaceMap = 436 SystemFonts.buildSystemTypefaces(fontConfig, fallback); 437 return Typeface.serializeFontMap(typefaceMap); 438 } catch (IOException | ErrnoException e) { 439 Slog.w(TAG, "Failed to serialize updatable font map. " 440 + "Retrying with system image fonts.", e); 441 return null; 442 } finally { 443 // Unmap buffers promptly, as we map a lot of files and may hit mmap limit before 444 // GC collects ByteBuffers and unmaps them. 445 for (ByteBuffer buffer : bufferCache.values()) { 446 if (buffer instanceof DirectByteBuffer) { 447 NioUtils.freeDirectBuffer(buffer); 448 } 449 } 450 } 451 } 452 453 @Nullable serializeSystemServerFontMap()454 private static SharedMemory serializeSystemServerFontMap() { 455 try { 456 return Typeface.serializeFontMap(Typeface.getSystemFontMap()); 457 } catch (IOException | ErrnoException e) { 458 Slog.e(TAG, "Failed to serialize SystemServer system font map", e); 459 return null; 460 } 461 } 462 setSerializedFontMap(SharedMemory serializedFontMap)463 private void setSerializedFontMap(SharedMemory serializedFontMap) { 464 SharedMemory oldFontMap = null; 465 synchronized (mSerializedFontMapLock) { 466 oldFontMap = mSerializedFontMap; 467 mSerializedFontMap = serializedFontMap; 468 } 469 if (oldFontMap != null) { 470 oldFontMap.close(); 471 } 472 } 473 } 474