Mojo “Style” Guide

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 to mojo@chromium.org.

For legacy IPC, please see security tips for IPC.

Simplicity

Strive to write simple interfaces. Minimize the amount of cross-process state that needs to be maintained in sync.

Good

interface TeleporterFactory {
  Create(Location start, Location end) => (Teleporter);
};

interface Teleporter {
  TeleportGoat(Goat) = ();
};

Bad

interface Teleporter {
  // 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

struct TeleporterStats {
  AnimalStats animal_stats;
  FungiStats fungi_stats;
  GoatStats goat_stats;
  PlantStats plant_stats;
};

interface Teleporter {
  TeleportAnimal(Animal) => ();
  TeleportFungi(Fungi) => ();
  TeleportGoat(Goat) = ();
  TeleportPlant(Plant) => ();

  // TeleportStats is only non-null if success is true.
  GetStats() => (bool success, TeleporterStats?);
};

Bad

interface Teleporter {
  // 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 sucess 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?);
};

Documentation

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 external README.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.
interface Teleporter {
  // 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 teleportation stats. On failure (e.g. a teleportation
  // operation is currently in progress) success will be false and a null stats
  // object will be returned.
  GetStats() =>
      (bool success, TeleportationStats?);
};

Security

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:

  1. Unprivileged process asks for a capability from the privileged process that owns the resource.
  2. Privileged process applies policy to find an implementation for the capability.
  3. Unprivileged process performs operations on the capability, constrained in scope.

The privileged process must own the capability lifecycle.

Do not trust less privileged processes

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:

  • the browser process is the most privileged process type and therefore, must be maximally suspicious of its IPC inputs
  • the renderer and the ARC++ processes are the least privileged and least trustworthy process types
  • other process types, such as GPU and plugin, fall in between

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

Do not send unnecessary or privilege-presuming data

Note: there is currently work in progress to associate origins with the InterfaceProviders for frames and workers: https://crbug.com/734210 and https://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, see RenderFrameHost::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 fixing https://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:

  1. Kill the renderer if basename(pathname) != pathname, since the renderer is obviously compromised if it makes this mistake.
  2. Defang the basename, by removing leading dots, et cetera. Note that the definition of proper defanging varies per platform.
  3. Prepend its own parent directory to the basename, e.g. ~/Downloads.

TODO(https://crbug.com/779196): Even better would be to implement a C++ type performs the appropriate sanitizations and recommend its usage directly here.

Do not define things that are not used

Platform-specific functionality should only be defined on the platforms where it is used.

The work to make this possible is in progress: https://crbug.com/676224

For enums, avoid the pattern of defining a LAST or MAX value. The LAST value is typically used in conjunction with legacy IPC macros to validate enums; this is not needed with Mojo enums, which automatically validated.

The MAX value is typically used as an invalid sentinel value for UMA histograms: unfortunately, simply defining a MAX value in a Mojo enum will cause Mojo to treat it as valid. This forces all IPC handling to do manual checks that the semantically invalid MAX value isn't accidentally or maliciously passed around.

Improving UMA logging is tracked in https://crbug.com/742517.

Use structured types

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:

  • Files: use mojo.common.mojom.File, not raw descriptor types like HANDLE and int.
  • File paths: use mojo.common.mojom.FilePath, not string.
  • JSON: use mojo.common.mojom.Value, not string.
  • Mojo interfaces: use Interface or Interface&, not handle or handle<message_pipe>.
  • Nonces: use mojo.common.mojom.UnguessableToken, not string.
  • Origins: use url.mojom.Origin, not url.mojom.Url and certainly not string.
  • Time types: use mojo.common.mojom.TimeDelta / mojo.common.mojom.TimeTicks / mojo.common.mojom.Time, not int64 / uint64 / double / et cetera.
  • URLs: use url.mojom.Url, not string.

Good

interface ReportingService {
  ReportDeprecation(mojo.common.mojom.TimeTicks time,
                    url.mojom.Url resource,
                    uint32 line_number);
};

Bad

interface ReportingService {
  // Bad: unclear what units |time| is or what |data| contains.
  ReportDeprecation(double time, mojo.common.mojom.Value data);
};

Another anti-pattern to avoid is parallel arrays of data: this requires the receiver to validate that all the arrays have the same length. Instead, prefer to pass the data so that it is impossible to have a mismatch.

Good

struct Pixel {
  int8 reds;
  int8 greens;
  int8 blues;
  int8 alphas;
};

struct Bitmap {
  // 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.
struct Bitmap {
  array<int8> reds;
  array<int8> greens;
  array<int8> blues;
  array<int8> alphas;
};

Beware of arithmetic overflow

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::ReportBadMessge("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);
}

C++ Best Practices

Use StructTraits

Creating a typemap and defining a StructTraits specialization moves the complexity of serialization, deserialization, and validation into a central location. We universally recommend this over defining TypeConverter 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_struct_traits.h:
template <>
struct StructTraits<url::mojom::UrlDataView, GURL> {
  static base::StringPiece url(const GURL& r);

  // If Read() returns false, Mojo will discard the message.
  static bool Read(url::mojom::UrlDataView data, GURL* out);
};

// In url_gurl_struct_traits.cc:
// Note that methods that aren't simple getters should be defined
// out-of-line to avoid code bloat.
base::StringPiece StructTraits<url::mojom::UrlDataView, GURL>::url(
    const GURL& r) {
  if (r.possibly_invalid_spec().length() > url::kMaxURLChars ||
      !r.is_valid()) {
    return base::StringPiece();
  }
  return base::StringPiece(r.possibly_invalid_spec().c_str(),
                           r.possibly_invalid_spec().length());
}

bool StructTraits<url::mojom::UrlDataView, GURL>::Read(
    url::mojom::UrlDataView data, GURL* out) {
  base::StringPiece url_string;
  if (!data.ReadUrl(&url_string))
    return false;
  if (url_string.length() > url::kMaxURLChars)
    return false;
  *out = GURL(url_string);
  return !url_string.empty() && out->is_valid();
}

Bad

template <>
struct TypeConverter<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 GURL Convert(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 corresponding EnumTraits and UnionTraits specializations for mojo enums and unions respectively.

StructTraits getters should be simple

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.

Out-of-line complex serialization/deserialization logic

A StructTraits specialization is almost always fully specialized. Only define StructTraits 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.

Do not write one-off functions to convert to/from Mojo types

There are some instances where it is simply not possible to define a StructTraits for type mapping: this commonly occurs with Blink IDL and Oilpan types. In these instances, add a TypeConverter 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 <>
struct TypeConverter<IDLDictionary, mojom::blink::DictionaryPtr> {
  static IDLDictionary* Convert(const mojom::blink::DictionaryPtr& in) {
    // Note that unlike StructTraits, there is no out-of-band way to signal
    // failure.
    IDLDictionary* out = new IDLDictionary;
    out->int_value = in->int_value;
    out->str_value = in->str_value;
    return out;
  }
};

Bad

// Using a custom one-off function makes this hard to discover in security
// audits.
IDLDictionary* FromMojo(const mojom::blink::DictionaryPtr& in) {
  IDLDictionary* out = new IDLDictionary;
  out->int_value = in->int_value;
  out->str_value = in->str_value;
  return out;
}

Use the proper abstractions

mojo::BindingSet 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 singular mojo::Binding and simply Close() the existing binding before reusing it.

Java Best Practices

Unfortunately, there are no strongly established conventions here. Most code tends to write manual conversion helpers and throw an exception on conversion failure. See NfcTypeConverter.java as one example of how to write conversion code.

General Code Health

Naming Conventions

Place mojo types in <top-level namespace>.mojom. Directories for Mojo traits should be named mojom (preferable) or ipc. Legacy names that are also encountered are public/interfaces, interfaces, or just mojo.

mojom is preferred for consistency between the directory name and the nested namespace name.

Do not handle impossible situations

Do not clutter the code by handling impossible situations. Omitting it makes the invariants clear. This takes two different forms:

  • A less trustworthy process can be compromised by an adversary and send arbitrary data. When processing data from a less trustworthy process, do not attempt to handle this invalid data: just call mojo::ReportBadMessage(). When invoked in the context of processing an IPC from the renderer, this will kill the renderer process.
  • A more trustworthy process must be trusted, by definition. Do not write code to handle impossible situations “just in case” there's a bug. For example, the renderer class 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.

Using mojo enums directly when possible

EnumTraits generally do not add much value: incoming Mojo enum values are already validated before typemapping, so it is guaranteed that the input value to EnumTraits<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.

Consider the cost of setting up message pipes

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.