Third-Party Licensing and the SBOM
Firefox ships a large amount of third-party code. Two artifacts describe it:
about:license, the user-visible attribution page, rendered at build time from theLICENSESdeclarations spread across the tree.A CycloneDX software bill of materials, generated by
./mach sbom, which describes the same code in a machine-readable form suitable for vulnerability scanners and compliance tooling.
Both read from the same declarations, so a notice added for about:license
also appears in the SBOM.
Declaring a License
A directory declares the notices it owns with the LICENSES and
LICENSED_UNDER moz.build variables. LICENSES introduces a notice and its
text; LICENSED_UNDER says which code that notice covers:
LICENSES += ["harfbuzz"]
LICENSES["harfbuzz"].title = "HarfBuzz License"
LICENSES["harfbuzz"].text = "LICENSE-NOTICE.txt"
LICENSED_UNDER += ["MIT"]
LICENSED_UNDER["MIT"].paths = ["vendor/lodash.js"]
The full reference for both variables, including how a notice’s text is
escaped and how shared notices are declared once in
toolkit/content/licenses/, is in
the mozbuild symbol reference.
What paths Covers
LICENSED_UNDER += ["MIT"] on its own attributes the whole declaring
directory to the MIT notice, and that is the common case. The paths flag
narrows that to the entries listed, leaving the rest of the directory
unattributed:
# every file in this directory is under MIT
LICENSED_UNDER += ["MIT"]
# only these two are; the rest of the directory is not
LICENSED_UNDER += ["MIT"]
LICENSED_UNDER["MIT"].paths = ["vendor/lodash.js", "vendor/react*"]
Entries are relative to the declaring moz.build, may be shell-style globs,
and a directory stands for its whole subtree. They do not have to be part of
the build: this is how a subtree the build system never traverses, a crate
under third_party/rust for instance, gets attributed from its nearest built
ancestor. Whatever is listed is joined onto the declaring directory, so what
reaches licenses.json, about:license and the SBOM is always
topsrcdir-relative.
LICENSES has a paths flag too, with the same meaning but a different
base: it is relative to the top source directory, because a shared notice is
declared far from the code it covers:
# in toolkit/content/licenses/moz.build
LICENSES["apache"].paths = ["third_party/perfetto"]
The two contribute to the same list, so a notice’s heading in about:license
shows the union of its own paths and every LICENSED_UNDER naming its id.
Prefer LICENSED_UNDER, which lives next to the code and moves with it;
LICENSES["x"].paths is for the shared notices that have nowhere local to be
declared.
Because a directory is only traversed in configurations that build it, a
notice is only recorded in builds that actually ship the code it covers. This
replaces the per-entry #ifdefs the hand-written license.html used to carry.
The build backend aggregates every declaration reachable in the current
configuration into <objdir>/licenses.json, which is what both
about:license and the SBOM consume.
For the same reason, a LICENSED_UNDER id whose LICENSES declaration lives
in a directory this configuration does not traverse is dropped rather than
treated as an error: a JS shell build reaches js/ and intl/ but never
toolkit/content/licenses/. An id that is declared nowhere at all is a typo,
and the license-declarations linter catches it because it reads every moz.build in the
tree regardless of configuration:
./mach lint -l license .
Declaring the same id twice is a build failure, reported by the backend.
The same linter validates every spdx flag as an SPDX license expression, so
Apache2 or BSD-3 is reported rather than shipped in the SBOM. Compound
expressions such as MIT OR Apache-2.0 are accepted, and a license with no
SPDX id can use LicenseRef-<name>.
It also reports an spdx flag declared inside a vendored library whose
moz.yaml already sets origin.license. That field is the one the SBOM
reports, so the flag would be a second copy of the same fact with nothing
keeping the two equal – and they had already drifted for media/libyuv,
declared BSD-3-Clause in moz.build against BSD-3-Clause-Clear in
moz.yaml. Drop the flag and let moz.yaml answer. The exception is a notice
covering code whose license differs from the library’s own, such as the MySpell
files inside hunspell, which says so explicitly and keeps its flag:
LICENSES["myspell"].spdx = "BSD-2-Clause"
LICENSES["myspell"].subcomponent = True
The flag also reaches the SBOM: the enclosing library’s component carries the
subcomponent’s expression alongside the one moz.yaml declares, instead of
moz.yaml’s answer standing for code it does not cover.
The notice text duplicates the same way. A vendored library already names the
file it ships its license in, in origin.license-file, so leave text unset
and the notice is read from there:
origin:
license: MIT
license-file: COPYING
LICENSES += ["expat"]
LICENSES["expat"].title = "Expat License"
The manifest is found by walking up from the declaring moz.build’s
directory, so this only works when the moz.build sits in the library’s
directory or below it. A moz.build that builds libraries vendored into its
subdirectories sits above their manifests, so it sets text to each library’s
license file instead.
The linter reports a text naming the file that manifest already names. Where
moz.yaml has no license-file, add it rather than copying the text into a
LICENSE-NOTICE.txt next to the moz.build: the manifest is what mach vendor
checks against upstream, so it is the copy that stays current.
A separate notice file earns its place when it is not a copy of a single
shipped file. media/libvpx needs one because its notice is LICENSE plus the
VP8 patent grant, which upstream keeps in libvpx/PATENTS; netwerk/sctp
needs one because its notice reproduces the FreeBSD and Cisco copyright headers
its sources carry, which upstream’s LICENSE.md does not.
Rendering about:license Without a Build
GENERATED_FILES renders the page during a build, but the whole page can also
be produced directly from the tree:
./mach licenses -o /tmp/license.html
This is the page the current configuration would ship. The tree has to be
configured, because the configuration decides which directories are traversed
and so which notices exist, but nothing beyond ./mach configure is needed:
the command reads the moz.build files itself rather than waiting for a build
to write licenses.json.
The command deliberately offers no machine-readable output. That is what
mach sbom is for.
Generating the SBOM
./mach sbom -o /tmp/sbom.json
The output is CycloneDX JSON. Useful arguments:
--strictExit non-zero if any
moz.yamlfails to load, rather than skipping it.--versionVersion to record for the product. Defaults to the configuration’s
MOZ_APP_VERSION_DISPLAY, or tobrowser/config/version_display.txtin an unconfigured tree.--product-nameName to record for the product. Defaults to the configuration’s
MOZ_APP_BASENAME–Firefoxfor desktop,Fennecfor GeckoView – or toFirefoxin an unconfigured tree.
What Becomes a Component
Components come from three sources, because none alone covers the tree:
moz.yamlmanifests give a name, an upstream version and revision, a description, upstream URLs and a Bugzilla component, but only exist for libraries thatmach vendormanages.Cargo.lockdescribesthird_party/rust, whichmoz.yamldoes not: one component per third-party crate, with its exact version, apkg:cargopackage URL, the SHA-256 crates.io publishes for the.cratearchive, and the crate-to-crate dependency edges. Licenses, descriptions and URLs come from the vendored crate’s ownCargo.toml. Workspace members are skipped — they are Firefox’s own crates, not third-party code.cargo metadatasupplies whatCargo.lockcannot: whether each crate is reached as a normal, a build or a dev dependency. Fifteen crates today are test-only,mockallandexpect-testamong them, and ship in nothing. The kinds are collected and reported on stderr but not yet written to the document; expressing them as CycloneDXscopeis the obvious next step. They need the objdir’s generated cargo config, so an unconfigured tree collects nothing and says so.LICENSESdeclarations cover everything else: one component per notice whose paths no manifest or crate already covers, carrying the notice id and the SPDX expression where one is known.
Where several describe the same code they are merged: the notice ids land on the manifest’s or the crate’s component, and where neither declared a license the notice’s SPDX expression fills the gap.
A handful of notices name no path at all — the MPL, the bundled spellchecking dictionaries, jQuery and React among them — so they cannot attach to a component. Those go on the root component instead.
The result is a superset of about:license: every notice id and every
attributed path is represented.
Identifiers, Evidence and the Graph
Each component’s bom-ref is the topsrcdir-relative path it was derived from —
the manifest’s directory, or third_party/rust/<crate> — and
license:<notice-id> for a component that came from a notice alone.
Package URLs are pkg:cargo for crates, and pkg:github or pkg:gitlab where
a manifest’s upstream repository is recognised, since pkg:generic matches
nothing in OSV.dev or the GitHub Advisory Database; everything else keeps the
upstream repository in a vcs_url qualifier. A notice-derived component gets
no package URL at all: it is a set of files in our own tree, not a package any
ecosystem can resolve, and a pkg:generic/<basename> would match nothing while
looking like it might.
A notice-derived component records the files it covers as CycloneDX
evidence.occurrences, one entry per path, rather than as one sibling
component per file: thirty files under one notice are one piece of third-party
code, and splitting them would bury the real libraries.
dependencies is a real graph wherever something knows one. The crates depend
on each other as Cargo.lock says, and everything no other component depends
on hangs off the root, so a viewer that draws the graph — CycloneDX Sunshine,
for instance — shows the crate tree rather than one flat ring of siblings.
Mozilla-Specific Properties
Metadata CycloneDX has no field for is recorded as moz:-prefixed properties:
Property |
Meaning |
|---|---|
|
The manifest this component came from |
|
Where to file bugs |
|
Free-form |
|
|
|
The |
|
For components derived from |
|
|
|
On the root component |
Reproducibility
Two runs over the same checkout produce byte-identical output. The BOM’s serial
number is derived from the source revision, and the timestamp defaults to the
head commit time rather than the wall clock. SOURCE_DATE_EPOCH overrides the
timestamp where release engineering sets it.
Without a Configured Objdir
licenses.json is written by the build backend, so on an unconfigured tree the
SBOM is built from moz.yaml alone and the command says so on stderr. That
covers vendored libraries only, considerably less than about:license
describes. Run ./mach build-backend for the complete picture.
In Automation
Shippable builds generate the SBOM as part of the build and upload it alongside the other build artifacts:
public/build/sbom.json
MOZ_GENERATE_SBOM: "1" in a build task’s worker.env turns on the configure
option of the same name, which adds a GENERATED_FILES entry for
<objdir>/sbom.json to the top-level moz.build. The build graph schedules it
like any other generated file, and automation/upload picks the result up. The
generator is strict there, so an unparseable moz.yaml fails the build rather
than silently shrinking the SBOM. The variable is set per task in
taskcluster/kinds/build/, so whether a given build produces an SBOM is
visible in the task definition.
Generating it per build, rather than once for the tree, is what makes the result correct: the declarations reachable in a configuration are the ones that configuration ships, so each platform’s artifact describes that platform. On macOS that means the per-architecture build tasks, not the universal task that only recombines their output. The usual index routes reach the latest one, for example:
https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.v2.mozilla-central.shippable.latest.firefox.linux64-opt/artifacts/public/build/sbom.json
about:license is not published separately; it ships inside the browser.