Mojo is Chrome's new IPC system and provides lots of useful abstractions. These abstractions can make it easier to write code that makes interprocess calls, but can also add significant complexity. Below are some recommendation from Mojo and IPC reviewers for best practices.
For questions, concerns, or suggestions, reach out tochromium-mojo@chromium.org.
For legacy IPC, please seesecurity tips for IPC.
Strive to write simple interfaces. Minimize the amount of cross-process state that needs to be maintained in sync.
Good
interfaceTeleporterFactory{Create(Location start,Locationend)=>(pending_remote<Teleporter>);};interfaceTeleporter{TeleportGoat(Goat)=();};
Bad
interfaceTeleporter{// Bad: comments will need to explicitly call out that both locations need to// be bound before calling TeleportGoat()!//// In addition, if untrustworthy processes can talk to trustworthy processes,// the Teleporter implementation will need to also handle the case where the// Location objects are not yet bound.SetStart(Location);SetEnd(Location);TeleportGoat(Goat)=();};
Similarly, strive to make methods focused. Do not overuse optional types.
Good
structTeleporterStats{AnimalStats animal_stats;FungiStats fungi_stats;GoatStats goat_stats;PlantStats plant_stats;};interfaceTeleporter{TeleportAnimal(Animal)=>();TeleportFungi(Fungi)=>();TeleportGoat(Goat)=();TeleportPlant(Plant)=>();// TeleporterStats will be have a value if and only if the call was// successful.GetStats()=>(TeleporterStats?);};
Bad
interfaceTeleporter{// The intent of four optional arguments is unclear: can this call teleport// multiple objects of different types at once, or is the caller only// supposed to only pass one non-null argument per call?Teleport(Animal?,Fungi?,Goat?,Plant?)=>();// Does this return all stats if success is true? Or just the categories that// the teleporter already has stats for? The intent is uncertain, so wrapping// the disparate values into a result struct would be cleaner.GetStats()=>(bool success,AnimalStats?,FungiStats?,PlantStats?,FungiStats?);};
Mojo structs, interfaces, and methods should all have comments. Make sure the comments cover the “how” and the “why” of using an interface and its methods, and not just the “what”. Document preconditions, postconditions, and trust: if an interface is implemented in the browser process and handles requests from the renderer process, this should be mentioned in the comments. Complex features should also have an externalREADME.md
that covers the high-level flow of information through interfaces and how they interact to implement the feature.
Good
// Interface for controlling a teleporter. Lives in the browser process, and// used to implement the Teleportation over Mojo IPC RFC.interfaceTeleporter{// Teleportation helpers for different taxonomic kingdoms. Teleportation is// not complete until the reply callback is invoked. The caller must NOT// release the sender side resources until the reply callback runs; releasing// the resources early will cause splinching.TeleportAnimal(Animal)=>();TeleportFungi(Fungi)=>();// Goats require a specialized teleportation channel distinct from// TeleportAnimal to ensure goatiness isolation.TeleportGoat(Goat)=>();TeleportPlant(Plant)=>();// Returns current teleporter stats. On failure (e.g. a teleportation// operation is currently in progress) a null stats object will be returned.GetStats()=>(TeleporterStats?);};
Policy should be controlled solely by the browser process. “Policy” can mean any number of things, such as sizes, addresses, permissions, URLs, origins, etc. In an ideal world:
The privileged process must own the capability lifecycle.
This is the overriding principle for all guidelines in this section. When receiving data from a less trusted process, treat the data as if it were generated by a malicious adversary. Message handlers cannot assume that offsets are valid, calculations won't overflow, et cetera.
In general:
When passing objects up a privilege gradient (from less → more privileged), the callee must validate the inputs before acting on them. When passing objects down a privilege gradient, such as from browser → renderer, it is OK for the callee to trust the caller.
See also:Do not Handle Impossible Situations
Each
BrowserInterfaceBroker
for frames and workers is strongly associated with an origin. Where possible, prefer to use this associated origin rather than sending it over IPC. (Seehttps://crbug.com/734210 andhttps://crbug.com/775792/).
For example, the browser process must not (fully) trust the renderer's claims about origins. The browser process should already know what origin the renderer is evaluating, and thus should already have this data (for example, seeRenderFrameHost::GetLastCommittedOrigin()
). Thus, a method that requires passing an origin from the renderer to the browser process has a conceptual error, and quite possibly, a vulnerability.
Note: there are currently subtle races when using
GetLastCommittedOrigin()
that will be resolved by fixinghttps://crbug.com/729021.
Similarly, the browser process must not trust the renderer's claims about file pathnames. It would be unsafe for the browser process to save a downloaded file to~/.bashrc
just because the renderer asked. Instead, it would be better for the browser process to:
basename(pathname) != pathname
, since the renderer is obviously compromised if it makes this mistake.TODO(crbug.com/41352236): Even better would be to implement a C++ type performs the appropriate sanitizations and recommend its usage directly here.
If it is not possible to avoid sending privilege-presuming data over IPC (see the previous section), then such data should be verified before being used.
ChildProcessSecurityPolicy
's methods likeCanAccessDataForOrigin
orCanReadFile
to verify IPC messages received from less privileged processes.mojo::ReportBadMessage
(or usingmojo::GetBadMessageCallback
for messages handled asynchronously). For legacy IPC, the renderer process may be terminated by calling theReceivedBadMessage
function (separate implementations exist for//content
,//chrome
and other layers).Mojo interfaces often cross privilege boundaries. Having well-defined interfaces that don't contain stubbed out methods or unused parameters makes it easier to understand and evaluate the implications of crossing these boundaries. Several common areas to watch out for:
Platform-specific functionality should only be defined on the platforms where it is implemented. Use the MojoEnableIf
annotation to guard definitions that should only be visible in certain build configurations.
Good
// GN file:mojom("view_bindings"){// ... enabled_features=[]if(is_android){ enabled_features+=["is_android"]}}// mojom definition:interfaceView{// ...[EnableIf=is_android]UpdateBrowserControlsState(bool enable_hiding,bool enable_showing,bool animate);};// C++ implementation:classView:public mojom::View{public:// ...#if BUILDFLAG(IS_ANDROID)voidUpdateBrowserControlsState(bool enable_hiding,bool enable_showing,bool animate);#endif};
Bad
// mojom definition:interfaceView{// ...UpdateBrowserControlsState(bool enable_hiding,bool enable_showing,bool animate);};// C++ implementation:classView:public mojom::View{public:// ...#if BUILDFLAG(IS_ANDROID)voidUpdateBrowserControlsState(bool enable_hiding,bool enable_showing,bool animate)override;#elsevoidUpdateBrowserControlsState(bool enable_hiding,bool enable_showing,bool animate)override{ NOTREACHED();}#endif};
TheEnableIf
annotation can be applied to almost anything: imports, interfaces, methods, arguments, constants, structs, struct members, enums, enumerator values, et cetera.
Reviewing IPC requires reviewing a concrete implementation of the Mojo interface, to evaluate how the (possibly untrustworthy) inputs are used, what outputs are produced, et cetera. If a method is not yet implemented, do not define it in the interface.
Bad
// mojom definition:interfaceSpaceship{EnterHyperspace();ExitHyperspace();};// C++ implementation:classSpaceshipPrototype:public mojom::Spaceship{voidEnterHyperspace(){/* TODO(dcheng): Implement. */}voidExitHyperspace(){/* TODO(dcheng): Implement. */}};
Do not define placeholder enumerator values likekLast
,kMax
,kCount
, et cetera. Instead, rely on the autogeneratedkMaxValue
enumerator emitted for Mojo C++ bindings.
For UMA histograms, logging a Mojo enum is simple: simply use the two argument version ofUMA_HISTOGRAM_ENUMERATION
:
Good
// mojom definition:enumGoatStatus{ kHappy, kSad, kHungry, kGoaty,};// C++:UMA_HISTOGRAM_ENUMERATION("Goat.Status", status);
Using akCount
sentinel complicatesswitch
statements and makes it harder to enforce invariants: code needs to actively enforce that the otherwise invalidkCount
sentinel value is not incorrectly passed around.
Bad
// mojom definition:enumCatStatus{ kAloof, kCount,};// C++switch(cat_status){caseCatStatus::kAloof:IgnoreHuman();break;caseCatStatus::kCount:// this should never happen}
DefiningkLast
manually results in ugly casts to perform arithmetic:
Bad
// mojom definition:enumWhaleStatus{ kFail, kNotFail, kLast= kNotFail,};// C++:UMA_HISTOGRAM_ENUMERATION("Whale.Status", status,static_cast<int>(WhaleStatus::kLast)+1);
For interoperation with legacy IPC, also usekMaxValue
rather than defining a customkLast
:
Good
IPC_ENUM_TRAITS_MAX_VALUE(GoatStatus,GoatStatus::kMaxValue);
Where possible, use structured types: this allows the type system to help enforce that the input data is valid. Common ones to watch out for:
mojo_base.mojom.File
, not raw descriptor types likeHANDLE
andint
.mojo_base.mojom.FilePath
, notstring
.mojo_base.mojom.Value
, notstring
.Interface
orInterface&
, nothandle
orhandle<message_pipe>
.mojo_base.mojom.UnguessableToken
, notstring
.url.mojom.Origin
, noturl.mojom.Url
and certainly notstring
.mojo_base.mojom.TimeDelta
/mojo_base.mojom.TimeTicks
/mojo_base.mojom.Time
, notint64
/uint64
/double
/ et cetera.mojo_base.mojom.JSTime
for times coming from Javascript Date objects.url.mojom.Url
, notstring
.array<uint8>
orstring
andmemcpy()
: use a Mojo struct and statically define the serialized fields. Whilememcpy()
may be tempting for its simplicity, it can leak info in padding. Even worse,memcpy()
can easily copyundocumented fields or newly introduced fields that were never evaluated for safety by the developer or reviewer.Good
interfaceReportingService{ReportDeprecation(mojo_base.mojom.TimeTicks time, url.mojom.Url resource, uint32 line_number);};
Bad
interfaceReportingService{// Bad: unclear what units |time| is or what |data| contains.ReportDeprecation(double time, mojo_base.mojom.Value data);};
Avoid parallel arrays of data that require the receiver to validate that the arrays have matching lengths. Instead, bundle the data together in a struct so it is impossible to have a mismatch:
Good
structPixel{ int8 reds; int8 greens; int8 blues; int8 alphas;};structBitmap{// Good: it is impossible for there to be mismatched data. array<Pixel> pixels;};
Bad
// Bad: code using this struct will need to validate that all the arrays have// matching sizes.structBitmap{ array<int8> reds; array<int8> greens; array<int8> blues; array<int8> alphas;};
TODO(dcheng): Import the guidance from the legacy IPC doc.
Signed overflow is undefined in C++. If unsure about whether or not something will overflow, use the safe numeric helpers from//base/numerics
!
Good
base::CheckedNumeric<int32_t> size= mojo_rect->width();size*= mojo_rect.height();if(!size.IsValid()){ mojo::ReportBadMessage("Bad size from renderer!");}
Bad
// Bad: Signed overflow is undefined in C++!int32_t size= mojo_rect->width()* mojo_rect.height();
Note that even if the types have defined overflow semantics, it is almost always a good idea to check for overflow.
Good
uint32_t alloc_size;if(!CheckMul(request->elements(), request->element_size()).AssignIfValid(&alloc_size)){// Safe: avoids allocating with a bogus size that overflowed to a smaller than// expected value. mojo::ReportBadMessage("Invalid allocation size");}Element* array=CreateArray(alloc_size);for(size_t i=0; i< request->element_size();++i){ array[i]=PopulateArray(i);}
Bad
uint32_t alloc_size= request->elements()* request->element_size();// Dangerous: alloc_size can overflow so that CreateArray allocates too little// memory. Subsequent assignments will turn into an out-of-bound write!Element* array=CreateArray(alloc_size);for(size_t i=0; i< request->element_size();++i){ array[i]=PopulateArray(i);}
When possible, messages should be defined in such a way that all possible values are semantically valid. As a corollary, avoid having the value of one field dictate the validity of other fields.
Good
unionCreateTokenResult{// Implies success.string token;// Implies failure.string error_message;};structTokenManager{CreateToken()=>(CreateTokenResult result);};
Bad
structTokenManager{// Requires caller to handle edge case where |success| is set to true, but// |token| is null.CreateToken()=>(bool success,string? token,string? error_message);// Requires caller to handle edge case where both |token| and |error_message|// are set, or both are null.CreateToken()=>(string? token,string? error_message);};
A known exception where we tolerate imperfect message semantics is with weakly typed integerbitfields.
Mojom has no native support for bitfields. There are two common approaches: a type-safe struct of bools which is a bit clunky (preferred) and an integer-based approach (allowed but not preferred).
Type-safe bitfields
structVehicleBits{bool has_car;bool has_bicycle;bool has_boat;};structPerson{VehicleBits bits;};
Integer based approach
structPerson{const uint64 kHasCar=1;const uint64 kHasBicycle=2;const uint64 kHasGoat=4; uint32 vehicle_bitfield;};
In both cases, consider typemapping these mojo types to your preferred C++ construct (e.g.base::StrongAlias<...>
,base::EnumSet<...>
, etc.) to improve downstream readability and type safety.
When creating newMojo services in the browser process (exposed to the renderer viaBrowserInterfaceBrokers
in a host object likeRenderFrameHostImpl
,DedicatedWorkerHost
, etc.), one approach is to have the interface implementation be owned by theReceiver
usingmojo::MakeSelfOwnedReceiver
. From themojo::MakeSelfOwnedReceiver
declaration:
// Binds the lifetime of an interface implementation to the lifetime of the// Receiver. When the Receiver is disconnected (typically by the remote end// closing the entangled Remote), the implementation will be deleted.
Consider such an interface created inRenderFrameHostImpl
, and consider that and a correspondingRemote
was created for this interface and owned byRenderFrame
. It may seem logical to think that:
Receiver
owns the interface implementationReceiver
is based on the lifetime of theRemote
in the rendererRemote
is owned by theRenderFrame
RenderFrameHostImpl
is based on the lifetime of theRenderFrame
RenderFrame
will cause theRemote
to be destroyed, ultimately causing theReceiver
and the interface implementation to be destroyed. TheRenderFrameHostImpl
will likely be destroyed at some point as well.RenderFrameHostImpl
will outlive the self-ownedReceiver
and interface implementationAcommon mistake based on the last assumption above is to store and use a raw pointer to theRenderFrameHostImpl
object in the interface implementation. If theReceiver
outlives theRenderFrameHostImpl
and uses the pointer to it, a Use-After-Free will occur. One way a malicious site or compromised renderer could make this happen is to generate lots of messages to the interface and then close the frame. TheReceiver
might have a backlog of messages to process before it gets the message indicating that the renderer'sRemote
was closed, and theRenderFrameHostImpl
can be destroyed in the meantime.
Similarly, it's not safe to assume that theProfile
object (and objects owned by it;StoragePartitionImpl
, for instance) will outlive theReceiver
. This has been observed to be true for at least incognito windows, where a renderer can generate messages, close the page, and cause the entire window to close (assuming no other pages are open), ultimately causing theOffTheRecordProfileImpl
object to be destroyed before theReceiver
object.
To avoid these types of issues, some solutions include:
UsingDocumentService
orDocumentUserData
instead ofmojo::MakeSelfOwnedReceiver
for document-based interfaces where the interface implementation needs access to aRenderFrameHostImpl
object. See theDocumentService
declaration for more details.
Having theReceiver
and/or interface implementation be owned by the object it relies on (for instance, store theReceiver
in a private member or use amojo::UniqueReceiverSet
for storing multipleReceiver
/ interface implementation pairs).
UsingWeakPtr
s instead of raw pointers to provide a way to check whether an object has been destroyed.
Mojo provides several convenience helpers to automatically invoke a callback if the callback has not already been invoked in some other way when the callback is destroyed, e.g.:
{base::OnceCallback<int> cb= mojo::WrapCallbackWithDefaultInvokeIfNotRun(base::BindOnce([](int){...}),-1);}// |cb| is automatically invoked with an argument of -1.
This can be useful for detecting interface errors:
process->GetMemoryStatistics( mojo::WrapCallbackWithDefaultInvokeIfNotRun(base::BindOnce(&MemoryProfiler::OnReplyFromRenderer),<failure args>));// If the remote process dies, &MemoryProfiler::OnReplyFromRenderer will be// invoked with <failure args> when Mojo drops outstanding callbacks due to// a connection error on |process|.
However, due to limitations of the current implementation, it's difficult to tell if a callback object has invoke-on-destroy behavior. In general:
Note that using the callback wrappers in the renderer is often unnecessary. Message pipes are typically closed as part of a Document shutting down; since many Blink objects already inherit
blink::ContextLifecycleObserver
, it is usually more idiomatic to use this signal to perform any needed cleanup work.
Creating a typemap and defining aStructTraits
specialization moves the complexity of serialization, deserialization, and validation into a central location. We universally recommend this over definingTypeConverter
specializations: when a value fails deserialization, the receiver method will never even be invoked. As a bonus, it also reduces the number of copies during serialization and deserialization. 😄
Good
// In url_gurl_mojom_traits.h:template<>structStructTraits<url::mojom::UrlDataView, GURL>{staticbase::StringPiece url(const GURL& r);// If Read() returns false, Mojo will discard the message.staticboolRead(url::mojom::UrlDataView data, GURL*out);};// In url_gurl_mojom_traits.cc:// Note that methods that aren't simple getters should be defined// out-of-line to avoid code bloat.base::StringPieceStructTraits<url::mojom::UrlDataView, GURL>::url(const GURL& r){if(r.possibly_invalid_spec().length()> url::kMaxURLChars||!r.is_valid()){returnbase::StringPiece();}returnbase::StringPiece(r.possibly_invalid_spec().c_str(), r.possibly_invalid_spec().length());}boolStructTraits<url::mojom::UrlDataView, GURL>::Read( url::mojom::UrlDataView data, GURL*out){base::StringPiece url_string;if(!data.ReadUrl(&url_string))returnfalse;if(url_string.length()> url::kMaxURLChars)returnfalse;*out= GURL(url_string);if(!url_string.empty()&&!out->is_valid())returnfalse;returntrue;}
Bad
template<>structTypeConverter<url::mojom::UrlPtr, GURL>{// Inefficient: this copies data once off the wire to create a// url.mojom.Url object, then copies it again to create a GURL.static GURLConvert(const url::mojom::UrlPtr url){if(url.url.is_empty())return GURL();// Not good: no way to signal errors, so any code that converts the// Mojo struct to a GURL will somehow need to check for errors…// but it can't even be distinguished from the empty URL case!if(url.url.size()> url::kMaxURLChars)return GURL();return GURL(url.url);}};
There are also correspondingEnumTraits
andUnionTraits
specializations for mojo enums and unions respectively.
Where possible,StructTraits
should be returning const references or simple read-only views of the data. Having to create temporary data structures during serialization should be rare, and it should be even rarer to mutate the input argument.
AStructTraits
specialization is almost always fully specialized. Only defineStructTraits
methods inline in the header if the method is a simple getter that returns a reference, pointer, or other simple POD. Define all other methods out-of-line to avoid code bloat.
There are some instances where it is simply not possible to define aStructTraits
for type mapping: this commonly occurs with Blink IDL and Oilpan types. In these instances, add aTypeConverter
specialization rather than defining a one-off conversion function. This makes it easier to search for and audit code that does potentially risky type conversions.
The use of
TypeConverter
should be limited as much as possible: ideally, only use it in renderers.
Good
template<>structTypeConverter<IDLDictionary, mojom::blink::DictionaryPtr>{staticIDLDictionary*Convert(const mojom::blink::DictionaryPtr&in){// Note that unlike StructTraits, there is no out-of-band way to signal// failure.IDLDictionary*out=newIDLDictionary;out->int_value=in->int_value;out->str_value=in->str_value;returnout;}};
Bad
// Using a custom one-off function makes this hard to discover in security// audits.IDLDictionary*FromMojo(const mojom::blink::DictionaryPtr&in){IDLDictionary*out=newIDLDictionary;out->int_value=in->int_value;out->str_value=in->str_value;returnout;}
mojo::ReceiverSet
implies multiple clients may connect. If this actually isn't the case, please do not use it. For example, if an interface can be rebound, then use the singularmojo::Receiver
and simplyreset()
the existing receiver before reusing it.
While validation should be done insideStructTraits
specializations when possible, there are situations where additional checks, e.g. overflow checks, are needed outside ofStructTraits
specializations. Usemojo::ReportBadMessage()
ormojo::GetBadMessageCallback()
to reject bad input in these situations. Under the hood, this may record UMAs, kill the process sending bad input, et cetera.
mojo::ReportBadMessage()
: use to report bad IPC input while a message is being dispatched on the stack.mojo::GetBadMessageCallback()
: use to generate a callback to report bad IPC input. The callback must be generated while a message is being dispatched on the stack; however, the returned callback may be invoked be freely invoked in asynchronously posted callbacks.Unfortunately, there are no strongly established conventions here. Most code tends to write manual conversion helpers and throw an exception on conversion failure. SeeNfcTypeConverter.java as one example of how to write conversion code.
Place mojo types in<top-level namespace>.mojom
. Directories for Mojo traits should be namedmojom
(preferable) oripc
. Legacy names that are also encountered arepublic/interfaces
,interfaces
, or justmojo
.
mojom
is preferred for consistency between the directory name and the nested namespace name.
Do not clutter the code by handling impossible situations. Omitting it makes the invariants clear. This takes two different forms:
mojo::ReportBadMessage()
. When invoked in the context of processing an IPC from the renderer, this will kill the renderer process.content::RenderFrameImpl
must always be connected to certain control interfaces in the browser. It does not makes sense to handle a Mojo connection error and try to reconnect: a connection error signals that the browser process is in the process of deleting the frame, and any attempt at reconnecting will never succeed.EnumTraits
generally do not add much value: incoming Mojo enum values are already validated before typemapping, so it is guaranteed that the input value toEnumTraits<T>::FromMojom()
is already a valid enum value, so the method itself is just a bunch of boilerplate to map between two very similarly named, yet slightly different, enums.
To avoid this, prefer to use the Mojo enum directly when possible.
Message pipes are fairly inexpensive, but they are not free either: it takes 6 control messages to establish a message pipe. Keep this in mind: if the interface is used relatively frequently, connecting once and reusing the interface pointer is probably a good idea.
BigBuffer uses shared memory to make passing large messages fast. When shmem is backing the message, it may be writable in the sending process while being read in the receiving process. If a BigBuffer is received from an untrustworthy process, you should make a copy of the data before processing it to avoid time-of-check time-of-use (TOCTOU) bugs. The |size()| of the data cannot be manipulated.
WebUI renderers sometimes need to call special, powerful IPC endpoints in a privileged process. It is important to enforce the constraint that the privileged callee previously created and blessed the calling process as a WebUI process, and not as a (potentially compromised) web renderer or other low-privilege process.
MojoWebUIController
. WebUI Mojo interfaces must only be exposed through aMojoWebUIController
subclass.MojoWebUIController
, to avoid circumventing access checks.Sometimes, there will be powerful new features that are not yet turned on by default, such as behind a flag, Finch trial, ororigin trial. It is not safe to check for the feature's availability on the renderer side (or in another low-privilege process type). Instead, ensure that the check is done in the process that has power to actually enact the feature. Otherwise, a compromised renderer could opt itself in to the feature! If the feature might not yet be fully developed and safe, vulnerabilities could arise.