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