1/*
2 * Copyright (C) 2017 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
17package android.webkit;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.ActivityManagerInternal;
22import android.content.pm.ApplicationInfo;
23import android.content.pm.PackageInfo;
24import android.os.Build;
25import android.os.Process;
26import android.os.RemoteException;
27import android.os.SystemProperties;
28import android.text.TextUtils;
29import android.util.Log;
30
31import com.android.internal.annotations.VisibleForTesting;
32import com.android.server.LocalServices;
33
34import dalvik.system.VMRuntime;
35
36import java.io.File;
37import java.io.IOException;
38import java.util.Arrays;
39import java.util.zip.ZipEntry;
40import java.util.zip.ZipFile;
41
42/**
43 * @hide
44 */
45@VisibleForTesting
46public class WebViewLibraryLoader {
47    private static final String LOGTAG = WebViewLibraryLoader.class.getSimpleName();
48
49    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_32 =
50            "/data/misc/shared_relro/libwebviewchromium32.relro";
51    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_64 =
52            "/data/misc/shared_relro/libwebviewchromium64.relro";
53    private static final long CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES = 100 * 1024 * 1024;
54
55    private static final boolean DEBUG = false;
56
57    private static boolean sAddressSpaceReserved = false;
58
59    /**
60     * Private class for running the actual relro creation in an unprivileged child process.
61     * RelroFileCreator is a static class (without access to the outer class) to avoid accidentally
62     * using any static members from the outer class. Those members will in reality differ between
63     * the child process in which RelroFileCreator operates, and the app process in which the static
64     * members of this class are used.
65     */
66    private static class RelroFileCreator {
67        // Called in an unprivileged child process to create the relro file.
68        public static void main(String[] args) {
69            boolean result = false;
70            boolean is64Bit = VMRuntime.getRuntime().is64Bit();
71            try {
72                if (args.length != 1 || args[0] == null) {
73                    Log.e(LOGTAG, "Invalid RelroFileCreator args: " + Arrays.toString(args));
74                    return;
75                }
76                Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), lib: " + args[0]);
77                if (!sAddressSpaceReserved) {
78                    Log.e(LOGTAG, "can't create relro file; address space not reserved");
79                    return;
80                }
81                result = nativeCreateRelroFile(args[0] /* path */,
82                                               is64Bit ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 :
83                                                         CHROMIUM_WEBVIEW_NATIVE_RELRO_32);
84                if (result && DEBUG) Log.v(LOGTAG, "created relro file");
85            } finally {
86                // We must do our best to always notify the update service, even if something fails.
87                try {
88                    WebViewFactory.getUpdateServiceUnchecked().notifyRelroCreationCompleted();
89                } catch (RemoteException e) {
90                    Log.e(LOGTAG, "error notifying update service", e);
91                }
92
93                if (!result) Log.e(LOGTAG, "failed to create relro file");
94
95                // Must explicitly exit or else this process will just sit around after we return.
96                System.exit(0);
97            }
98        }
99    }
100
101    /**
102     * Create a single relro file by invoking an isolated process that to do the actual work.
103     */
104    static void createRelroFile(final boolean is64Bit, @NonNull WebViewNativeLibrary nativeLib) {
105        final String abi =
106                is64Bit ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0];
107
108        // crashHandler is invoked by the ActivityManagerService when the isolated process crashes.
109        Runnable crashHandler = new Runnable() {
110            @Override
111            public void run() {
112                try {
113                    Log.e(LOGTAG, "relro file creator for " + abi + " crashed. Proceeding without");
114                    WebViewFactory.getUpdateService().notifyRelroCreationCompleted();
115                } catch (RemoteException e) {
116                    Log.e(LOGTAG, "Cannot reach WebViewUpdateService. " + e.getMessage());
117                }
118            }
119        };
120
121        try {
122            if (nativeLib == null || nativeLib.path == null) {
123                throw new IllegalArgumentException(
124                        "Native library paths to the WebView RelRo process must not be null!");
125            }
126            boolean success = LocalServices.getService(ActivityManagerInternal.class)
127                    .startIsolatedProcess(
128                            RelroFileCreator.class.getName(), new String[] { nativeLib.path },
129                            "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler);
130            if (!success) throw new Exception("Failed to start the relro file creator process");
131        } catch (Throwable t) {
132            // Log and discard errors as we must not crash the system server.
133            Log.e(LOGTAG, "error starting relro file creator for abi " + abi, t);
134            crashHandler.run();
135        }
136    }
137
138    /**
139     * Perform preparations needed to allow loading WebView from an application. This method should
140     * be called whenever we change WebView provider.
141     * @return the number of relro processes started.
142     */
143    static int prepareNativeLibraries(PackageInfo webviewPackageInfo)
144            throws WebViewFactory.MissingWebViewPackageException {
145        WebViewNativeLibrary nativeLib32bit =
146                getWebViewNativeLibrary(webviewPackageInfo, false /* is64bit */);
147        WebViewNativeLibrary nativeLib64bit =
148                getWebViewNativeLibrary(webviewPackageInfo, true /* is64bit */);
149        updateWebViewZygoteVmSize(nativeLib32bit, nativeLib64bit);
150
151        return createRelros(nativeLib32bit, nativeLib64bit);
152    }
153
154    /**
155     * @return the number of relro processes started.
156     */
157    private static int createRelros(@Nullable WebViewNativeLibrary nativeLib32bit,
158            @Nullable WebViewNativeLibrary nativeLib64bit) {
159        if (DEBUG) Log.v(LOGTAG, "creating relro files");
160        int numRelros = 0;
161
162        if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
163            if (nativeLib32bit == null) {
164                Log.e(LOGTAG, "No 32-bit WebView library path, skipping relro creation.");
165            } else {
166                if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro");
167                createRelroFile(false /* is64Bit */, nativeLib32bit);
168                numRelros++;
169            }
170        }
171
172        if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
173            if (nativeLib64bit == null) {
174                Log.e(LOGTAG, "No 64-bit WebView library path, skipping relro creation.");
175            } else {
176                if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro");
177                createRelroFile(true /* is64Bit */, nativeLib64bit);
178                numRelros++;
179            }
180        }
181        return numRelros;
182    }
183
184    /**
185     *
186     * @return the native WebView libraries in the new WebView APK.
187     */
188    private static void updateWebViewZygoteVmSize(
189            @Nullable WebViewNativeLibrary nativeLib32bit,
190            @Nullable WebViewNativeLibrary nativeLib64bit)
191            throws WebViewFactory.MissingWebViewPackageException {
192        // Find the native libraries of the new WebView package, to change the size of the
193        // memory region in the Zygote reserved for the library.
194        long newVmSize = 0L;
195
196        if (nativeLib32bit != null) newVmSize = Math.max(newVmSize, nativeLib32bit.size);
197        if (nativeLib64bit != null) newVmSize = Math.max(newVmSize, nativeLib64bit.size);
198
199        if (DEBUG) {
200            Log.v(LOGTAG, "Based on library size, need " + newVmSize
201                    + " bytes of address space.");
202        }
203        // The required memory can be larger than the file on disk (due to .bss), and an
204        // upgraded version of the library will likely be larger, so always attempt to
205        // reserve twice as much as we think to allow for the library to grow during this
206        // boot cycle.
207        newVmSize = Math.max(2 * newVmSize, CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES);
208        Log.d(LOGTAG, "Setting new address space to " + newVmSize);
209        setWebViewZygoteVmSize(newVmSize);
210    }
211
212    /**
213     * Reserve space for the native library to be loaded into.
214     */
215    static void reserveAddressSpaceInZygote() {
216        System.loadLibrary("webviewchromium_loader");
217        long addressSpaceToReserve =
218                SystemProperties.getLong(WebViewFactory.CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY,
219                CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES);
220        sAddressSpaceReserved = nativeReserveAddressSpace(addressSpaceToReserve);
221
222        if (sAddressSpaceReserved) {
223            if (DEBUG) {
224                Log.v(LOGTAG, "address space reserved: " + addressSpaceToReserve + " bytes");
225            }
226        } else {
227            Log.e(LOGTAG, "reserving " + addressSpaceToReserve + " bytes of address space failed");
228        }
229    }
230
231    /**
232     * Load WebView's native library into the current process.
233     *
234     * <p class="note"><b>Note:</b> Assumes that we have waited for relro creation.
235     *
236     * @param clazzLoader class loader used to find the linker namespace to load the library into.
237     * @param libraryFileName the filename of the library to load.
238     */
239    public static int loadNativeLibrary(ClassLoader clazzLoader, String libraryFileName) {
240        if (!sAddressSpaceReserved) {
241            Log.e(LOGTAG, "can't load with relro file; address space not reserved");
242            return WebViewFactory.LIBLOAD_ADDRESS_SPACE_NOT_RESERVED;
243        }
244
245        String relroPath = VMRuntime.getRuntime().is64Bit() ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 :
246                                                              CHROMIUM_WEBVIEW_NATIVE_RELRO_32;
247        int result = nativeLoadWithRelroFile(libraryFileName, relroPath, clazzLoader);
248        if (result != WebViewFactory.LIBLOAD_SUCCESS) {
249            Log.w(LOGTAG, "failed to load with relro file, proceeding without");
250        } else if (DEBUG) {
251            Log.v(LOGTAG, "loaded with relro file");
252        }
253        return result;
254    }
255
256    /**
257     * Fetch WebView's native library paths from {@param packageInfo}.
258     * @hide
259     */
260    @Nullable
261    @VisibleForTesting
262    public static WebViewNativeLibrary getWebViewNativeLibrary(PackageInfo packageInfo,
263            boolean is64bit) throws WebViewFactory.MissingWebViewPackageException {
264        ApplicationInfo ai = packageInfo.applicationInfo;
265        final String nativeLibFileName = WebViewFactory.getWebViewLibrary(ai);
266
267        String dir = getWebViewNativeLibraryDirectory(ai, is64bit /* 64bit */);
268
269        WebViewNativeLibrary lib = findNativeLibrary(ai, nativeLibFileName,
270                is64bit ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS, dir);
271
272        if (DEBUG) {
273            Log.v(LOGTAG, String.format("Native %d-bit lib: %s", is64bit ? 64 : 32, lib.path));
274        }
275        return lib;
276    }
277
278    /**
279     * @return the directory of the native WebView library with bitness {@param is64bit}.
280     * @hide
281     */
282    @VisibleForTesting
283    public static String getWebViewNativeLibraryDirectory(ApplicationInfo ai, boolean is64bit) {
284        // Primary arch has the same bitness as the library we are looking for.
285        if (is64bit == VMRuntime.is64BitAbi(ai.primaryCpuAbi)) return ai.nativeLibraryDir;
286
287        // Secondary arch has the same bitness as the library we are looking for.
288        if (!TextUtils.isEmpty(ai.secondaryCpuAbi)) {
289            return ai.secondaryNativeLibraryDir;
290        }
291
292        return "";
293    }
294
295    /**
296     * @return an object describing a native WebView library given the directory path of that
297     * library, or null if the library couldn't be found.
298     */
299    @Nullable
300    private static WebViewNativeLibrary findNativeLibrary(ApplicationInfo ai,
301            String nativeLibFileName, String[] abiList, String libDirectory)
302            throws WebViewFactory.MissingWebViewPackageException {
303        if (TextUtils.isEmpty(libDirectory)) return null;
304        String libPath = libDirectory + "/" + nativeLibFileName;
305        File f = new File(libPath);
306        if (f.exists()) {
307            return new WebViewNativeLibrary(libPath, f.length());
308        } else {
309            return getLoadFromApkPath(ai.sourceDir, abiList, nativeLibFileName);
310        }
311    }
312
313    /**
314     * @hide
315     */
316    @VisibleForTesting
317    public static class WebViewNativeLibrary {
318        public final String path;
319        public final long size;
320
321        WebViewNativeLibrary(String path, long size) {
322            this.path = path;
323            this.size = size;
324        }
325    }
326
327    private static WebViewNativeLibrary getLoadFromApkPath(String apkPath,
328                                                           String[] abiList,
329                                                           String nativeLibFileName)
330            throws WebViewFactory.MissingWebViewPackageException {
331        // Search the APK for a native library conforming to a listed ABI.
332        try (ZipFile z = new ZipFile(apkPath)) {
333            for (String abi : abiList) {
334                final String entry = "lib/" + abi + "/" + nativeLibFileName;
335                ZipEntry e = z.getEntry(entry);
336                if (e != null && e.getMethod() == ZipEntry.STORED) {
337                    // Return a path formatted for dlopen() load from APK.
338                    return new WebViewNativeLibrary(apkPath + "!/" + entry, e.getSize());
339                }
340            }
341        } catch (IOException e) {
342            throw new WebViewFactory.MissingWebViewPackageException(e);
343        }
344        return null;
345    }
346
347    /**
348     * Sets the size of the memory area in which to store the relro section.
349     */
350    private static void setWebViewZygoteVmSize(long vmSize) {
351        SystemProperties.set(WebViewFactory.CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY,
352                Long.toString(vmSize));
353    }
354
355    static native boolean nativeReserveAddressSpace(long addressSpaceToReserve);
356    static native boolean nativeCreateRelroFile(String lib, String relro);
357    static native int nativeLoadWithRelroFile(String lib, String relro, ClassLoader clazzLoader);
358}
359