WebViewFactory.java revision 6ce92c9362143d2f5b4a52c14520528ef2c1bbd5
1/*
2 * Copyright (C) 2012 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.SystemApi;
20import android.app.ActivityManagerInternal;
21import android.app.AppGlobals;
22import android.app.Application;
23import android.content.Context;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.os.Build;
28import android.os.Process;
29import android.os.RemoteException;
30import android.os.ServiceManager;
31import android.os.StrictMode;
32import android.os.SystemProperties;
33import android.os.Trace;
34import android.text.TextUtils;
35import android.util.AndroidRuntimeException;
36import android.util.Log;
37
38import com.android.server.LocalServices;
39
40import dalvik.system.VMRuntime;
41
42import java.io.File;
43import java.io.IOException;
44import java.util.Arrays;
45import java.util.zip.ZipEntry;
46import java.util.zip.ZipFile;
47
48/**
49 * Top level factory, used creating all the main WebView implementation classes.
50 *
51 * @hide
52 */
53@SystemApi
54public final class WebViewFactory {
55
56    private static final String CHROMIUM_WEBVIEW_FACTORY =
57            "com.android.webview.chromium.WebViewChromiumFactoryProvider";
58
59    private static final String NULL_WEBVIEW_FACTORY =
60            "com.android.webview.nullwebview.NullWebViewFactoryProvider";
61
62    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_32 =
63            "/data/misc/shared_relro/libwebviewchromium32.relro";
64    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_64 =
65            "/data/misc/shared_relro/libwebviewchromium64.relro";
66
67    public static final String CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY =
68            "persist.sys.webview.vmsize";
69    private static final long CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES = 100 * 1024 * 1024;
70
71    private static final String LOGTAG = "WebViewFactory";
72
73    private static final boolean DEBUG = false;
74
75    // Cache the factory both for efficiency, and ensure any one process gets all webviews from the
76    // same provider.
77    private static WebViewFactoryProvider sProviderInstance;
78    private static final Object sProviderLock = new Object();
79    private static boolean sAddressSpaceReserved = false;
80    private static PackageInfo sPackageInfo;
81
82    // Error codes for loadWebViewNativeLibraryFromPackage
83    public static final int LIBLOAD_SUCCESS = 0;
84    public static final int LIBLOAD_WRONG_PACKAGE_NAME = 1;
85    public static final int LIBLOAD_ADDRESS_SPACE_NOT_RESERVED = 2;
86    public static final int LIBLOAD_FAILED_WAITING_FOR_RELRO = 3;
87    public static final int LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES = 4;
88
89    // native relro loading error codes
90    public static final int LIBLOAD_FAILED_TO_OPEN_RELRO_FILE = 5;
91    public static final int LIBLOAD_FAILED_TO_LOAD_LIBRARY = 6;
92    public static final int LIBLOAD_FAILED_JNI_CALL = 7;
93
94    private static class MissingWebViewPackageException extends AndroidRuntimeException {
95        public MissingWebViewPackageException(String message) { super(message); }
96        public MissingWebViewPackageException(Exception e) { super(e); }
97    }
98
99    /** @hide */
100    public static String[] getWebViewPackageNames() {
101        return AppGlobals.getInitialApplication().getResources().getStringArray(
102                com.android.internal.R.array.config_webViewPackageNames);
103    }
104
105    // TODO (gsennton) remove when committing webview xts test change
106    public static String getWebViewPackageName() {
107        String[] webViewPackageNames = getWebViewPackageNames();
108        return webViewPackageNames[webViewPackageNames.length-1];
109    }
110
111    /**
112     * Return the package info of the first package in the webview priority list that contains
113     * webview.
114     *
115     * @hide
116     */
117    public static PackageInfo findPreferredWebViewPackage() {
118        PackageManager pm = AppGlobals.getInitialApplication().getPackageManager();
119
120        for (String packageName : getWebViewPackageNames()) {
121            try {
122                PackageInfo packageInfo = pm.getPackageInfo(packageName,
123                    PackageManager.GET_META_DATA);
124                ApplicationInfo applicationInfo = packageInfo.applicationInfo;
125
126                // If the correct flag is set the package contains webview.
127                if (getWebViewLibrary(applicationInfo) != null) {
128                    return packageInfo;
129                }
130            } catch (PackageManager.NameNotFoundException e) {
131            }
132        }
133        throw new MissingWebViewPackageException("Could not find a loadable WebView package");
134    }
135
136    // throws MissingWebViewPackageException
137    private static ApplicationInfo getWebViewApplicationInfo() {
138        if (sPackageInfo == null)
139            return findPreferredWebViewPackage().applicationInfo;
140        else
141            return sPackageInfo.applicationInfo;
142    }
143
144    private static String getWebViewLibrary(ApplicationInfo ai) {
145        if (ai.metaData != null)
146            return ai.metaData.getString("com.android.webview.WebViewLibrary");
147        return null;
148    }
149
150    public static PackageInfo getLoadedPackageInfo() {
151        return sPackageInfo;
152    }
153
154    /**
155     * Load the native library for the given package name iff that package
156     * name is the same as the one providing the webview.
157     */
158    public static int loadWebViewNativeLibraryFromPackage(String packageName) {
159        try {
160            sPackageInfo = findPreferredWebViewPackage();
161        } catch (MissingWebViewPackageException e) {
162            return LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES;
163        }
164
165        if (packageName != null && packageName.equals(sPackageInfo.packageName)) {
166            return loadNativeLibrary();
167        }
168        return LIBLOAD_WRONG_PACKAGE_NAME;
169    }
170
171    static WebViewFactoryProvider getProvider() {
172        synchronized (sProviderLock) {
173            // For now the main purpose of this function (and the factory abstraction) is to keep
174            // us honest and minimize usage of WebView internals when binding the proxy.
175            if (sProviderInstance != null) return sProviderInstance;
176
177            final int uid = android.os.Process.myUid();
178            if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID) {
179                throw new UnsupportedOperationException(
180                        "For security reasons, WebView is not allowed in privileged processes");
181            }
182
183            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
184            try {
185                Class<WebViewFactoryProvider> providerClass = getProviderClass();
186
187                StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
188                Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "providerClass.newInstance()");
189                try {
190                    sProviderInstance = providerClass.getConstructor(WebViewDelegate.class)
191                            .newInstance(new WebViewDelegate());
192                    if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance);
193                    return sProviderInstance;
194                } catch (Exception e) {
195                    Log.e(LOGTAG, "error instantiating provider", e);
196                    throw new AndroidRuntimeException(e);
197                } finally {
198                    Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
199                    StrictMode.setThreadPolicy(oldPolicy);
200                }
201            } finally {
202                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
203            }
204        }
205    }
206
207    private static Class<WebViewFactoryProvider> getProviderClass() {
208        try {
209            // First fetch the package info so we can log the webview package version.
210            sPackageInfo = findPreferredWebViewPackage();
211            Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " +
212                sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")");
213
214            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
215            loadNativeLibrary();
216            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
217
218            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
219            try {
220                return getChromiumProviderClass();
221            } catch (ClassNotFoundException e) {
222                Log.e(LOGTAG, "error loading provider", e);
223                throw new AndroidRuntimeException(e);
224            } finally {
225                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
226            }
227        } catch (MissingWebViewPackageException e) {
228            // If the package doesn't exist, then try loading the null WebView instead.
229            // If that succeeds, then this is a device without WebView support; if it fails then
230            // swallow the failure, complain that the real WebView is missing and rethrow the
231            // original exception.
232            try {
233                return (Class<WebViewFactoryProvider>) Class.forName(NULL_WEBVIEW_FACTORY);
234            } catch (ClassNotFoundException e2) {
235                // Ignore.
236            }
237            Log.e(LOGTAG, "Chromium WebView package does not exist", e);
238            throw new AndroidRuntimeException(e);
239        }
240    }
241
242    // throws MissingWebViewPackageException
243    private static Class<WebViewFactoryProvider> getChromiumProviderClass()
244            throws ClassNotFoundException {
245        Application initialApplication = AppGlobals.getInitialApplication();
246        try {
247            // Construct a package context to load the Java code into the current app.
248            Context webViewContext = initialApplication.createPackageContext(
249                    sPackageInfo.packageName,
250                    Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
251            initialApplication.getAssets().addAssetPath(
252                    webViewContext.getApplicationInfo().sourceDir);
253            ClassLoader clazzLoader = webViewContext.getClassLoader();
254            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "Class.forName()");
255            try {
256                return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true,
257                                                                     clazzLoader);
258            } finally {
259                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
260            }
261        } catch (PackageManager.NameNotFoundException e) {
262            throw new MissingWebViewPackageException(e);
263        }
264    }
265
266    /**
267     * Perform any WebView loading preparations that must happen in the zygote.
268     * Currently, this means allocating address space to load the real JNI library later.
269     */
270    public static void prepareWebViewInZygote() {
271        try {
272            System.loadLibrary("webviewchromium_loader");
273            long addressSpaceToReserve =
274                    SystemProperties.getLong(CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY,
275                    CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES);
276            sAddressSpaceReserved = nativeReserveAddressSpace(addressSpaceToReserve);
277
278            if (sAddressSpaceReserved) {
279                if (DEBUG) {
280                    Log.v(LOGTAG, "address space reserved: " + addressSpaceToReserve + " bytes");
281                }
282            } else {
283                Log.e(LOGTAG, "reserving " + addressSpaceToReserve +
284                        " bytes of address space failed");
285            }
286        } catch (Throwable t) {
287            // Log and discard errors at this stage as we must not crash the zygote.
288            Log.e(LOGTAG, "error preparing native loader", t);
289        }
290    }
291
292    /**
293     * Perform any WebView loading preparations that must happen at boot from the system server,
294     * after the package manager has started or after an update to the webview is installed.
295     * This must be called in the system server.
296     * Currently, this means spawning the child processes which will create the relro files.
297     */
298    public static void prepareWebViewInSystemServer() {
299        String[] nativePaths = null;
300        try {
301            nativePaths = getWebViewNativeLibraryPaths();
302        } catch (Throwable t) {
303            // Log and discard errors at this stage as we must not crash the system server.
304            Log.e(LOGTAG, "error preparing webview native library", t);
305        }
306        prepareWebViewInSystemServer(nativePaths);
307    }
308
309    private static void prepareWebViewInSystemServer(String[] nativeLibraryPaths) {
310        if (DEBUG) Log.v(LOGTAG, "creating relro files");
311
312        // We must always trigger createRelRo regardless of the value of nativeLibraryPaths. Any
313        // unexpected values will be handled there to ensure that we trigger notifying any process
314        // waiting on relreo creation.
315        if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
316            if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro");
317            createRelroFile(false /* is64Bit */, nativeLibraryPaths);
318        }
319
320        if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
321            if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro");
322            createRelroFile(true /* is64Bit */, nativeLibraryPaths);
323        }
324    }
325
326    public static void onWebViewUpdateInstalled() {
327        String[] nativeLibs = null;
328        try {
329            nativeLibs = WebViewFactory.getWebViewNativeLibraryPaths();
330            if (nativeLibs != null) {
331                long newVmSize = 0L;
332
333                for (String path : nativeLibs) {
334                    if (path == null || TextUtils.isEmpty(path)) continue;
335                    if (DEBUG) Log.d(LOGTAG, "Checking file size of " + path);
336                    File f = new File(path);
337                    if (f.exists()) {
338                        newVmSize = Math.max(newVmSize, f.length());
339                        continue;
340                    }
341                    if (path.contains("!/")) {
342                        String[] split = TextUtils.split(path, "!/");
343                        if (split.length == 2) {
344                            try (ZipFile z = new ZipFile(split[0])) {
345                                ZipEntry e = z.getEntry(split[1]);
346                                if (e != null && e.getMethod() == ZipEntry.STORED) {
347                                    newVmSize = Math.max(newVmSize, e.getSize());
348                                    continue;
349                                }
350                            }
351                            catch (IOException e) {
352                                Log.e(LOGTAG, "error reading APK file " + split[0] + ", ", e);
353                            }
354                        }
355                    }
356                    Log.e(LOGTAG, "error sizing load for " + path);
357                }
358
359                if (DEBUG) {
360                    Log.v(LOGTAG, "Based on library size, need " + newVmSize +
361                            " bytes of address space.");
362                }
363                // The required memory can be larger than the file on disk (due to .bss), and an
364                // upgraded version of the library will likely be larger, so always attempt to
365                // reserve twice as much as we think to allow for the library to grow during this
366                // boot cycle.
367                newVmSize = Math.max(2 * newVmSize, CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES);
368                Log.d(LOGTAG, "Setting new address space to " + newVmSize);
369                SystemProperties.set(CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY,
370                        Long.toString(newVmSize));
371            }
372        } catch (Throwable t) {
373            // Log and discard errors at this stage as we must not crash the system server.
374            Log.e(LOGTAG, "error preparing webview native library", t);
375        }
376        prepareWebViewInSystemServer(nativeLibs);
377    }
378
379    // throws MissingWebViewPackageException
380    private static String getLoadFromApkPath(String apkPath,
381                                             String[] abiList,
382                                             String nativeLibFileName) {
383        // Search the APK for a native library conforming to a listed ABI.
384        try (ZipFile z = new ZipFile(apkPath)) {
385            for (String abi : abiList) {
386                final String entry = "lib/" + abi + "/" + nativeLibFileName;
387                ZipEntry e = z.getEntry(entry);
388                if (e != null && e.getMethod() == ZipEntry.STORED) {
389                    // Return a path formatted for dlopen() load from APK.
390                    return apkPath + "!/" + entry;
391                }
392            }
393        } catch (IOException e) {
394            throw new MissingWebViewPackageException(e);
395        }
396        return "";
397    }
398
399    // throws MissingWebViewPackageException
400    private static String[] getWebViewNativeLibraryPaths() {
401        ApplicationInfo ai = getWebViewApplicationInfo();
402        final String NATIVE_LIB_FILE_NAME = getWebViewLibrary(ai);
403
404        String path32;
405        String path64;
406        boolean primaryArchIs64bit = VMRuntime.is64BitAbi(ai.primaryCpuAbi);
407        if (!TextUtils.isEmpty(ai.secondaryCpuAbi)) {
408            // Multi-arch case.
409            if (primaryArchIs64bit) {
410                // Primary arch: 64-bit, secondary: 32-bit.
411                path64 = ai.nativeLibraryDir;
412                path32 = ai.secondaryNativeLibraryDir;
413            } else {
414                // Primary arch: 32-bit, secondary: 64-bit.
415                path64 = ai.secondaryNativeLibraryDir;
416                path32 = ai.nativeLibraryDir;
417            }
418        } else if (primaryArchIs64bit) {
419            // Single-arch 64-bit.
420            path64 = ai.nativeLibraryDir;
421            path32 = "";
422        } else {
423            // Single-arch 32-bit.
424            path32 = ai.nativeLibraryDir;
425            path64 = "";
426        }
427
428        // Form the full paths to the extracted native libraries.
429        // If libraries were not extracted, try load from APK paths instead.
430        if (!TextUtils.isEmpty(path32)) {
431            path32 += "/" + NATIVE_LIB_FILE_NAME;
432            File f = new File(path32);
433            if (!f.exists()) {
434                path32 = getLoadFromApkPath(ai.sourceDir,
435                                            Build.SUPPORTED_32_BIT_ABIS,
436                                            NATIVE_LIB_FILE_NAME);
437            }
438        }
439        if (!TextUtils.isEmpty(path64)) {
440            path64 += "/" + NATIVE_LIB_FILE_NAME;
441            File f = new File(path64);
442            if (!f.exists()) {
443                path64 = getLoadFromApkPath(ai.sourceDir,
444                                            Build.SUPPORTED_64_BIT_ABIS,
445                                            NATIVE_LIB_FILE_NAME);
446            }
447        }
448
449        if (DEBUG) Log.v(LOGTAG, "Native 32-bit lib: " + path32 + ", 64-bit lib: " + path64);
450        return new String[] { path32, path64 };
451    }
452
453    private static void createRelroFile(final boolean is64Bit, String[] nativeLibraryPaths) {
454        final String abi =
455                is64Bit ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0];
456
457        // crashHandler is invoked by the ActivityManagerService when the isolated process crashes.
458        Runnable crashHandler = new Runnable() {
459            @Override
460            public void run() {
461                try {
462                    Log.e(LOGTAG, "relro file creator for " + abi + " crashed. Proceeding without");
463                    getUpdateService().notifyRelroCreationCompleted(is64Bit, false);
464                } catch (RemoteException e) {
465                    Log.e(LOGTAG, "Cannot reach WebViewUpdateService. " + e.getMessage());
466                }
467            }
468        };
469
470        try {
471            if (nativeLibraryPaths == null
472                    || nativeLibraryPaths[0] == null || nativeLibraryPaths[1] == null) {
473                throw new IllegalArgumentException(
474                        "Native library paths to the WebView RelRo process must not be null!");
475            }
476            int pid = LocalServices.getService(ActivityManagerInternal.class).startIsolatedProcess(
477                    RelroFileCreator.class.getName(), nativeLibraryPaths, "WebViewLoader-" + abi, abi,
478                    Process.SHARED_RELRO_UID, crashHandler);
479            if (pid <= 0) throw new Exception("Failed to start the relro file creator process");
480        } catch (Throwable t) {
481            // Log and discard errors as we must not crash the system server.
482            Log.e(LOGTAG, "error starting relro file creator for abi " + abi, t);
483            crashHandler.run();
484        }
485    }
486
487    private static class RelroFileCreator {
488        // Called in an unprivileged child process to create the relro file.
489        public static void main(String[] args) {
490            boolean result = false;
491            boolean is64Bit = VMRuntime.getRuntime().is64Bit();
492            try{
493                if (args.length != 2 || args[0] == null || args[1] == null) {
494                    Log.e(LOGTAG, "Invalid RelroFileCreator args: " + Arrays.toString(args));
495                    return;
496                }
497                Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), " +
498                        " 32-bit lib: " + args[0] + ", 64-bit lib: " + args[1]);
499                if (!sAddressSpaceReserved) {
500                    Log.e(LOGTAG, "can't create relro file; address space not reserved");
501                    return;
502                }
503                result = nativeCreateRelroFile(args[0] /* path32 */,
504                                               args[1] /* path64 */,
505                                               CHROMIUM_WEBVIEW_NATIVE_RELRO_32,
506                                               CHROMIUM_WEBVIEW_NATIVE_RELRO_64);
507                if (result && DEBUG) Log.v(LOGTAG, "created relro file");
508            } finally {
509                // We must do our best to always notify the update service, even if something fails.
510                try {
511                    getUpdateService().notifyRelroCreationCompleted(is64Bit, result);
512                } catch (RemoteException e) {
513                    Log.e(LOGTAG, "error notifying update service", e);
514                }
515
516                if (!result) Log.e(LOGTAG, "failed to create relro file");
517
518                // Must explicitly exit or else this process will just sit around after we return.
519                System.exit(0);
520            }
521        }
522    }
523
524    private static int loadNativeLibrary() {
525        if (!sAddressSpaceReserved) {
526            Log.e(LOGTAG, "can't load with relro file; address space not reserved");
527            return LIBLOAD_ADDRESS_SPACE_NOT_RESERVED;
528        }
529
530        try {
531            getUpdateService().waitForRelroCreationCompleted(VMRuntime.getRuntime().is64Bit());
532        } catch (RemoteException e) {
533            Log.e(LOGTAG, "error waiting for relro creation, proceeding without", e);
534            return LIBLOAD_FAILED_WAITING_FOR_RELRO;
535        }
536
537        try {
538            String[] args = getWebViewNativeLibraryPaths();
539            int result = nativeLoadWithRelroFile(args[0] /* path32 */,
540                                                     args[1] /* path64 */,
541                                                     CHROMIUM_WEBVIEW_NATIVE_RELRO_32,
542                                                     CHROMIUM_WEBVIEW_NATIVE_RELRO_64);
543            if (result != LIBLOAD_SUCCESS) {
544                Log.w(LOGTAG, "failed to load with relro file, proceeding without");
545            } else if (DEBUG) {
546                Log.v(LOGTAG, "loaded with relro file");
547            }
548            return result;
549        } catch (MissingWebViewPackageException e) {
550            Log.e(LOGTAG, "Failed to list WebView package libraries for loadNativeLibrary", e);
551            return LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES;
552        }
553    }
554
555    private static IWebViewUpdateService getUpdateService() {
556        return IWebViewUpdateService.Stub.asInterface(ServiceManager.getService("webviewupdate"));
557    }
558
559    private static native boolean nativeReserveAddressSpace(long addressSpaceToReserve);
560    private static native boolean nativeCreateRelroFile(String lib32, String lib64,
561                                                        String relro32, String relro64);
562    private static native int nativeLoadWithRelroFile(String lib32, String lib64,
563                                                          String relro32, String relro64);
564}
565