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