DevSecOps

GN and Meson Build Systems: Security

A side-by-side security comparison of GN (Chromium) and Meson, covering declarative posture, wrap files, toolchain handling, and supply chain behavior.

Nayan Dey
Senior Security Engineer
7 min read

Both GN and Meson emerged as attempts to fix the CMake ergonomics problem without giving up the C/C++ focus. Both generate build.ninja files (plus Visual Studio/Xcode projects). Both have gained significant production use. But their security properties are meaningfully different, and I have deployed both in supply-chain-sensitive contexts over the past two years. This post is the comparison I wish I had before those projects started.

I am going to keep this concrete: specific versions, specific config files, specific trade-offs. The goal is to help someone choose between them for a new project, or understand the risk profile of an existing one.

GN: Chromium's Build Language

GN ("Generate Ninja") is the build system Google wrote for Chromium when GYP started to scale poorly. It is Python-inspired, declarative, and tightly coupled to Ninja. GN itself is a small C++ binary that parses .gn files and emits .ninja files. It is not a general-purpose build tool in the way CMake is; it is optimized for large C++ codebases that want strong structure.

A minimal BUILD.gn:

import("//build/config/chrome_build.gni")

executable("payments_daemon") {
  sources = [
    "src/main.cc",
    "src/ledger.cc",
  ]
  deps = [
    "//base",
    "//net",
    "//third_party/boringssl",
  ]
  configs += [ "//build/config/compiler:wexit_time_destructors" ]
}

Three security-relevant properties:

  1. No arbitrary script execution at evaluation. GN's language is restricted. You cannot exec() a subprocess or read a file from disk during evaluation. This is stronger than CMake, which allows execute_process liberally.
  2. Explicit toolchain definitions. Every target declares a toolchain. Chromium's build uses pinned toolchain versions fetched via depot_tools, so the compiler is a project artifact, not an environment assumption.
  3. Strict visibility. GN's visibility and public_deps rules enforce that a target cannot silently depend on a non-exported internal.

For supply chain, the toolchain property matters most. A Chromium build on a random developer laptop uses the same clang version as CI, because the clang binary is fetched into src/third_party/llvm-build/ and referenced by path. There is no $PATH lookup, no "whichever gcc happens to be installed." This closes the class of attacks where a malicious compiler wrapper influences the build.

GN's Weaknesses

GN is not a package manager. Third-party dependencies are vendored into third_party/ and built in place. The update process is manual: a human runs roll-dep, checks in the new source, and the build picks up the changes. There is no lockfile because there is nothing being locked -- the dependency is the source checked into your repository.

This shifts the supply chain boundary from "what does the package manager give me" to "what does my third_party/ directory contain." It is a different problem, not necessarily a worse one. Chromium handles it through aggressive code review of third_party/ changes and automated scanning. For smaller projects, the same discipline is harder to maintain.

GN is also very Chromium-shaped. Using it outside Chromium requires porting build configs, which means maintaining a fork of //build. This is doable -- Fuchsia uses GN, as does the Dawn graphics library -- but it is work. For a new project, the GN ecosystem is narrow.

Meson: Smaller and More Approachable

Meson is a Python 3 build system that, like GN, emits Ninja files. It is used by GNOME, GStreamer, systemd, and the Mesa graphics stack. Meson 1.3.0 (released December 2023) is what I ran for a recent audit.

A minimal meson.build:

project('payments-daemon', 'cpp',
  version : '1.4.2',
  default_options : ['warning_level=3', 'cpp_std=c++20'])

boringssl_dep = dependency('boringssl', version : '>=0.0', required : true)
spdlog_dep = dependency('spdlog', version : '>=1.12')

executable('payments_daemon',
  sources : ['src/main.cc', 'src/ledger.cc'],
  dependencies : [boringssl_dep, spdlog_dep],
  install : true)

Meson's supply chain story is richer than GN's in one key area: the wrap file system. A subprojects/spdlog.wrap file looks like:

[wrap-file]
directory = spdlog-1.12.0
source_url = https://github.com/gabime/spdlog/archive/v1.12.0.tar.gz
source_filename = spdlog-1.12.0.tar.gz
source_hash = 4dccf2d10f410c1e2feaff89966bfc49a1abb29ef6f08246335b110e001e09a9
patch_directory = spdlog

[provide]
spdlog = spdlog_dep

The source_hash is enforced. Meson refuses to build if the downloaded file does not match, and it caches the file in subprojects/packagecache/ keyed by hash. This is roughly equivalent to Bazel Central Registry's integrity enforcement, done per-project.

Meson's WrapDB (https://wrapdb.mesonbuild.com/) is a small central index of approved wrap files for common dependencies. Use wraps from WrapDB by running meson wrap install spdlog, which fetches the wrap file, hash-verifies it, and drops it into subprojects/.

Meson's Weaknesses

Meson's evaluation is stricter than CMake but looser than GN. You cannot execute arbitrary shell, but you can call run_command() to invoke external tools, which means a malicious meson.build has a local code execution vector at configuration time. Treat meson.build files the same way you treat setup.py or Cargo.toml build scripts: inspect before running.

The Python implementation is a mixed blessing. It makes Meson easy to extend and debug, but it also means every Meson build ships with an implicit dependency on a working Python 3 interpreter. If your threat model includes supply chain attacks on Python packages, Meson is technically downstream of pypi in a way GN is not.

Third-party wraps are community-maintained. The quality varies. A wrap in WrapDB has been through some review, but wraps published elsewhere can contain anything. Pin to WrapDB, and be suspicious of wraps that live outside it.

Toolchain Handling: GN Wins

GN's explicit, path-based toolchains are genuinely more secure than Meson's cc detection logic. Meson finds the compiler by asking the environment ($CC, then $PATH). You can override with --native-file and --cross-file, which pin to absolute paths:

# cross-x86_64-linux.ini
[binaries]
c = '/opt/clang-17.0.6/bin/clang'
cpp = '/opt/clang-17.0.6/bin/clang++'
ar = '/opt/clang-17.0.6/bin/llvm-ar'
strip = '/opt/clang-17.0.6/bin/llvm-strip'

[properties]
sys_root = '/opt/sysroot-linux-x86_64'

Do this in CI and audit developer setups for consistency. Without --cross-file, Meson will silently use whatever compiler is on the build machine.

Dependency Resolution

GN has no dependency resolver. You either vendor a dependency or you don't.

Meson has three paths:

  1. System dependencies via pkg-config. Meson asks pkg-config for a library; the supply chain boundary is the system package manager.
  2. Subprojects via wrap files. The supply chain boundary is the hash in the wrap file.
  3. CMake subprojects via import('cmake'). This lets Meson wrap a CMake-built dependency, inheriting all of CMake's supply chain properties (usually not great).

For supply chain posture, prefer wrap files with hashes over system dependencies. Wrap files make the build self-contained and auditable; system dependencies push the trust boundary outward to an OS package manager whose integrity you may not control.

Reproducibility

Both GN and Meson produce reproducible builds with effort. The usual fixes apply: SOURCE_DATE_EPOCH, -ffile-prefix-map, LC_ALL=C, deterministic linker options.

Meson 1.2+ has built-in support for reproducible builds via b_reproducible=true (experimental as of 1.3). GN inherits reproducibility from Chromium's extensive work on the topic; is_official_build=true in Chromium gives bit-reproducible output on Linux when the toolchain is pinned.

For a greenfield project targeting reproducibility, both are workable. GN gives you stronger defaults; Meson gives you a faster path to get there without Chromium-scale infrastructure.

Choosing Between Them

Pick GN if:

  • You are building a Chromium fork or using Chromium-derived libraries.
  • Your project is large (millions of LOC) and you want declarative discipline.
  • You have infrastructure to maintain a pinned toolchain and vendored dependencies.

Pick Meson if:

  • You want a build system that integrates cleanly with existing Linux distribution packaging.
  • You have external dependencies you want to pull via wrap files rather than vendor.
  • Python 3 is already in your trusted base.

For most new C++ projects I advise on, Meson is the pragmatic choice. For projects that are serious about supply chain at Chromium's threat level, GN's model is more defensible.

How Safeguard Helps

Safeguard ingests both GN build graphs (via gn desc output) and Meson introspection data (meson introspect --all) to produce CycloneDX 1.5 SBOMs that capture every vendored subproject, wrap file, and pkg-config dependency with integrity context. Policy gates can enforce wrap file hash presence, block wraps sourced outside WrapDB, or require absolute toolchain paths in Meson cross-files. For GN-based projects, Safeguard correlates third_party/ directory changes with commit provenance to detect unexpected dependency rolls. The combination layers centralized governance on top of the per-project supply chain controls that GN and Meson provide natively, giving security teams visibility across projects that use either generator.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.