Design Document: Out Of Process V8-PAC execution
DRAFT: 2 August 2009
Eric Roman <email@example.com>
Do proxy resolving outside of the main browser process, in a dedicated sandboxed process.
What is PAC?
Proxy auto configuration (PAC) is a
method of configuring which proxy server(s) the browser should use when
issuing requests. These access rules are specified by a PAC file, using
URL), or it can locate one using the Web Proxy Autodiscovery Protocol
When configured to use a PAC script, the PAC script's FindProxyForURL()
Motivation — why move PAC out of process?
The current version of chromium supports PAC by running V8 in the browser process. This has the
downside that an exploit in V8 could be used to compromise the browser process.
Although PAC scripts are generally thought of as trusted executables (they intercept/modify your traffic), there are times when you may run them unknowingly — for example when joining a public wifi network.
So it is important that they can't p0wn your priviledged browser process.
A final concern is that V8 marks some memory pages as (readable + writable + executable). We don't want such memory pages in the browser process since it makes certain exploits easier. (http://crbug.com/11443.)
This document uses directed graphs to illustrate how code modules fit together.
An arrow from A to B means that A depends on B.
(Typically B is
the an interface, or a concrete implementation of an interface.)
Blue arrows are used for dependencies that cross threads (implies message posting):
Red arrows are used for dependencies that cross process boundaries (implies an IPC channel between process A and process B):
Quick introduction to the proxy subsystem
ProxyService is the front-end for proxy resolving.
It handles all the proxy details internally, delegating some tasks to its dependencies:
- Monitoring the system proxy settings for changes. [ProxyConfigService]
- Downloading PAC scripts. [ProxyScriptFetcher]
- PAC script evaluation. [ProxyResolver]
- Applying the proxy bypass lists
- Applying the proxy fallback rules, and remembering bad proxies
- Implements a subset of WPAD (auto-discovery)
ProxyResolverV8 is an implementation of ProxyResolver that uses V8 to run the PAC script.
It creates a PAC environment to run scripts in, and delegates external functionality to ProxyResolverJSBindings. In summary ProxyResolverJSBindings handles:
In process design (current)
In the current version of chromium, where proxy scripts are executed in-process, the pieces fit together as follows:
- SingleThreadedProxyResolver schedules proxy resolve requests in FIFO order to a single worker thread ("PAC thread").
- ProxyResolverV8 runs the PAC script's FindProxyForURL() function. Care is taken to lock around V8 in case it is being used elsewhere in the process (V8 is not threadsafe).
- If the script needs to resolve DNS, DefaultJSBindings bridges the DNS resolve request over to the "IO thread" where it can be serviced by HostResolverImpl in async mode (HostResolverImpl is not threadsafe).
- DefaultJSBindings pipes any JS errors and any alert() calls to the debug log.
Out of process design (proposed)
For out of process proxy resolving, the pieces will instead fit together as follows:
- IPCProxyResolver — new class that implements the ProxyResolver interface.
- Sends requests through an IPC channel to IPCProxyResolverHost.
- Receives requests from IPCProxyResolverHost through an IPC channel.
- IPCProxyResolverHost — main class that controls the proxy resolver process.
- Forwards the proxy resolve/cancel requests received through IPC to a ProxyResolverV8 instance (by way of a SingleThreadedProxyResolver wrapper).
- IPCProxyResolverHost::JSBindings — new class that implements ProxyResolverJSBindings.
- We can't implement the bindings directly in the proxy resolver process, since we lack priviledges to access the network (plus we want to share the browser's host cache).
- Edge (1) is defined by a set of IPC messages from the browser process to the proxy resolver process (see next section).
- Edge (3) corresponds to a set of IPC messages from the proxy resolving process to the browser process (see next section).
IPC messages from the browser process to the proxy resolver process
The following messages correspond to the ProxyResolver interface:
- Msg_GetProxyForURL_Start(int request_id, const GURL& url);
- Starts the proxy resolve for URL url. This request will be subsequently identified using request_id.
- On completion, the proxy resolver process sends us a Msg_GetProxyForURL_Complete message with the result.
- Msg_CancelRequest(int request_id);
- Msg_SetPacScriptByData_Start(int request_id, const std::string& bytes);
- Sends the proxy resolver a new PAC script given by bytes.
- On completion, the proxy resolver process sends us a Msg_SetPacScriptByData_Complete message with the result code.
- Subsequently started requests will be run through this PAC script.
- TODO(eroman): ProxyResolver::SetPacScriptByData does not take a callback yet...
So this and ProxyResolver::CancelSetPacScript only make sense in the context of the upcoming codereview:160510 which adds them.
- Msg_CancelSetPacScriptByData(int request_id);
The following messages correspond to the ProxyResolverJSBindings interface (ACKs):
- Msg_DnsResolve_Complete(int request_id, const std::string& resolved_ip);
- Msg_MyIpAddress_Complete(int request_id, const std::string& my_ip);
IPC messages from proxy resolver process to the browser process
The following messages correspond to the ProxyResolver interface (ACKs):
- Msg_GetProxyForURL_Complete(int request_id, int result_code, const ProxyInfo& results);
- Sent when request_id completed (and was not cancelled). result_code and results are the resuls of ProxyResolver::GetProxyForURL.
- Msg_SetPacScriptByData_Complete(int request_id, int result_code);
- Sent when request_id completed (and was not cancelled). result_code is the network error for the request.
The following messages correspond to the ProxyResolverJSBindings interface:
- Msg_Alert(request_id, const std::string& message);
- On completion, the browser process replies with a Msg_MyIpAddress_Complete message.
- Msg_DnsResolve_Start(int request_id, const std::string& host);
- On completion, the browser process replies with a Msg_DnsResolve_Complete message.
- Msg_OnError(int request_id, int line_number, const std::string& error);
Proxy resolver process
The lifetime of the proxy resolver process is controlled by IPCProxyResolver.
- The process is lazily created during calls to IPCProxyResolver::SetPacScriptByData(script_bytes).
- While the process is starting up, IPCProxyResolver will buffer requests. Once completed, the buffered requests are flushed down the IPC channel.
- The process is killed during ~IPCProxyResolver, and when we receive a Msg_SetPacScriptByData_Complete with an error code (indicating that there was a parse error in the script).
- The process is killed by brutally murdering it — there is no need to shut it down cleanly, as it doesn't persist any state. We can't be bothered to wait for it.
- The process will remain alive for as long as PAC execution may be used (i.e. until the settings change, or we fail setting the PAC script.).
- The proxy resolver process may be short-lived when using autodetect (the process can be rapidly created and then destroyed).
- This happens because on the first request, a proxy resolver process is spun up to test the candidate PAC script. Should the script fail ProxyResolver::SetPacScriptByData (i.e. parse error) then Msg_SetPacScriptByData_Complete returns an error code, and we destroy the process (see Teardown section). Of course this error is passed on to the ProxyService so it know to fallback to another setting.
When the proxy resolver process crashes, IPCProxyResolver notices the problem and responds with a failure for all of the requests which we were waiting from completion from the proxy resolver process.
- Log the crash to LOG(ERROR).
- Upload the crash report.
- Since the resolver process is lazily created, the next request made to IPCProxyResolver will recover by creating a new process.
- Had this been a tentative resolve by auto-detect, then ProxyService will fall back to other settings on the failure.
If the proxy resolver process is unresponsive, we murder it. This would then get treated like a crash (see above).
- We could watch for un-responsiveness by putting time-outs on outstanding IPCs (in case perhaps the PAC script never halts).
- Note: No plans to implement this for v1 — the user can always accomplish this manually by killing the proxy resolver process through task manager.
- The user-visible changes to UI:
- A proxy resolver process will show up in the "about:memory" page.
- A proxy resolver process will show up in the chrome task manager.
The disadvantage to doing PAC out of process is it may hurt performance:
- There is an added latency on the very first network request (waiting for the proxy resolving process to be lazily created.)
- There is additional latency for each network request (time spent doing 1 or more IPC roundtrips.)
- More memory is needed due to the extra process.
- Need to maintain some more helper data-structures, and the codesize is necessarily increased.
The most conservative approximation of the per-network-request latency overhead (for a given URL) is:
(1 + num_myIpAddress + num_dnsResolves) * kIpcRoundtripLatency
- kIpcRoundtripLatency is the cost of an IPC message roundtrip, measured in seconds.
- TODO(eroman): what is an empirical measure for average IPC latency? Maybe O(1ms) ?
Commonly corporate PAC scripts will have num_myIpAddress > 1 and num_dnsResolve > 1.
At first glance this looks like a lot of overhead.
However, there are several effective optimizations which we can follow-up with to bring the number of sequential IPC roundtrips to just 1.
- Cache the default interface's IP address in the proxy resolver process.
- This effectively makes num_myIpAddress = 0.
- Keep a host cache in the proxy resolver process.
- The number of unique hosts that PAC scripts try to resolve is pretty much guaranteed to be < 1. So already by having a local cache we have bounded num_dnsResolve <= 1. And moreover once you through cache hit ratios into the mix, we expect for num_dnsResolves < 1.
Finally, if caches are insufficient, we can play other tricks:
- If the browser notices that the particular PAC script is always requesting DNS resolves for the URL's host, it can begin pre-emptively starting the host resolve (browser-side) in parallel to the IPC request to the proxy resolver process.
This seems like overkill though, since if we got a cache miss in the proxy resolver host cache, we would be forced to use the system's proxy resolver which is super slow anyway (making the cost of an extra IPC insignificant in comparison).
The out of process code will live in a new proxy directory in the chrome module.
Note that the out of process proxy resolving code is part of the chrome module and NOT the net module. This is because starting up processes and talking over IPC are not concepts of the net module, nor should they be.
The cross-module integration is straightforward: ProxyService's constructor takes a ProxyResolver dependency, so we just have to pass it an instance of the new IPCProxyResolver. No need to teach
the net module anything further about multi-process stuff.
Testing will be done as in-process-ui tests, and will focus on request cancellation, and startup/teardown of the proxy resolver process.
- (expected) Reviewers/Collaborators: