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