diff --git a/MODULE.bazel b/MODULE.bazel index 7b202a8c28f..474a8dc4814 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,6 +15,7 @@ bazel_dep(name = "rules_cc", version = "0.2.25") bazel_dep(name = "llvm", version = "0.8.24") bazel_dep(name = "libbpf", version = "1.7.0.bcr.1") bazel_dep(name = "rules_shell", version = "0.8.0") +bazel_dep(name = "rules_qemu", version = "0.3.0") bazel_dep(name = "grpc", version = "1.84.0", repo_name = "com_github_grpc_grpc") # gRPC's older minimum imports a removed Google APIs extension repository. @@ -198,42 +199,18 @@ http_file( ) # Statically-linked QEMU user-mode emulators. -QEMU_USER_EXPORTS = 'exports_files(["usr/bin/qemu-aarch64", "usr/bin/qemu-riscv64", "usr/bin/qemu-x86_64"])' - -deb_data = use_repo_rule("//tools/bazeldefs:extensions/deb_data.bzl", "deb_data") - -http_archive( - name = "qemu_user_amd64", - build_file_content = 'exports_files(["data.tar.xz"])', - sha256 = "ca6ede739327a20ae5a3498230c8a3a9a072c586e131bc6bcc1201eb1725e336", - type = "deb", - urls = [ - "https://snapshot.debian.org/archive/debian/20260905T205002Z/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_amd64.deb", - "https://deb.debian.org/debian/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_amd64.deb", - ], -) - -deb_data( - name = "qemu_user_amd64_files", - build_file_content = QEMU_USER_EXPORTS, - data = "@qemu_user_amd64//:data.tar.xz", -) - -http_archive( - name = "qemu_user_arm64", - build_file_content = 'exports_files(["data.tar.xz"])', - sha256 = "c73711af02b97cd5e2667e735d9c06890c57165517110ca4e646c95a7083158d", - type = "deb", - urls = [ - "https://snapshot.debian.org/archive/debian/20260905T205002Z/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_arm64.deb", - "https://deb.debian.org/debian/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_arm64.deb", - ], +qemu = use_extension("@rules_qemu//qemu/extension:qemu.bzl", "qemu") +use_repo( + qemu, + "qemu_system_toolchains", + "qemu_user_toolchains", ) -deb_data( - name = "qemu_user_arm64_files", - build_file_content = QEMU_USER_EXPORTS, - data = "@qemu_user_arm64//:data.tar.xz", +register_toolchains( + "@qemu_user_toolchains//:qemu_linux_aarch64_on_linux_x86_64", + "@qemu_user_toolchains//:qemu_linux_riscv64_on_linux_aarch64", + "@qemu_user_toolchains//:qemu_linux_riscv64_on_linux_x86_64", + "@qemu_user_toolchains//:qemu_linux_x86_64_on_linux_aarch64", ) # Root certificates. diff --git a/governance/licensing.yaml b/governance/licensing.yaml index 73c81bee151..737b0c7867d 100644 --- a/governance/licensing.yaml +++ b/governance/licensing.yaml @@ -60,15 +60,159 @@ exceptions: test containers and is not linked into released gVisor binaries. Its source archive and GPLv2 license are retained with the build inputs, and the test image includes the license text. - - dependency: qemu_user_amd64 + - dependency: qemu_user_prebuilt_linux_amd64_aarch64 license: GPL-2.0-only exception_rationale: >- - QEMU user-mode emulators for x86_64 build machines, run as a build tool - to execute binaries built for the target architecture when - cross-compiling. QEMU is neither linked into nor distributed with gVisor. - - dependency: qemu_user_arm64 + QEMU user-mode emulator for aarch64 target binaries on amd64 build + machines. The executable uses QEMU's GPL terms, not the MIT license of + the prebuilt repository's build scripts. Used only for cross-compilation; + QEMU is neither linked into nor distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_arm license: GPL-2.0-only exception_rationale: >- - QEMU user-mode emulators for arm64 build machines, run as a build tool - to execute binaries built for the target architecture when - cross-compiling. QEMU is neither linked into nor distributed with gVisor. + QEMU user-mode emulator for arm target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_i386 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for i386 target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_mips64 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for mips64 target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_ppc + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for ppc target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_ppc64le + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for ppc64le target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_riscv32 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for riscv32 target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_riscv64 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for riscv64 target binaries on amd64 build + machines. The executable uses QEMU's GPL terms, not the MIT license of + the prebuilt repository's build scripts. Used only for cross-compilation; + QEMU is neither linked into nor distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_s390x + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for s390x target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_amd64_x86_64 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for x86_64 target binaries on amd64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_aarch64 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for aarch64 target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_arm + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for arm target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_i386 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for i386 target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_mips64 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for mips64 target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_ppc + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for ppc target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_ppc64le + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for ppc64le target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_riscv32 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for riscv32 target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_riscv64 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for riscv64 target binaries on arm64 build + machines. The executable uses QEMU's GPL terms, not the MIT license of + the prebuilt repository's build scripts. Used only for cross-compilation; + QEMU is neither linked into nor distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_s390x + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for s390x target binaries on arm64 build + machines, declared by the user toolchain alongside the selected + cross-compilers. The executable uses QEMU's GPL terms, not the MIT + license of its build scripts. QEMU is neither linked into nor + distributed with gVisor. + - dependency: qemu_user_prebuilt_linux_arm64_x86_64 + license: GPL-2.0-only + exception_rationale: >- + QEMU user-mode emulator for x86_64 target binaries on arm64 build + machines. The executable uses QEMU's GPL terms, not the MIT license of + the prebuilt repository's build scripts. Used only for cross-compilation; + QEMU is neither linked into nor distributed with gVisor. diff --git a/pkg/seccomp/precompiledseccomp/defs.bzl b/pkg/seccomp/precompiledseccomp/defs.bzl index 3741d32f690..55c519d3ced 100644 --- a/pkg/seccomp/precompiledseccomp/defs.bzl +++ b/pkg/seccomp/precompiledseccomp/defs.bzl @@ -2,6 +2,53 @@ load("//tools:defs.bzl", "go_binary", "select_arch", "target_emulator") +def _run_generator_impl(ctx): + generator = ctx.attr.exec_generator or ctx.attr.generator + executable = generator[DefaultInfo].files_to_run + tools = [] + arguments = [] + qemu = None + toolchain = None + if not ctx.attr.exec_generator: + if ctx.attr.qemu: + qemu = ctx.attr.qemu[DefaultInfo].files_to_run + elif target_emulator.toolchain_type: + qemu_info = ctx.toolchains[target_emulator.toolchain_type] + if qemu_info: + qemu = qemu_info.qemu + toolchain = target_emulator.toolchain_type + if qemu: + arguments.extend(ctx.attr.qemu_args) + tools.append(executable) + arguments.append(executable.executable.path) + executable = qemu + arguments.extend([ + "--package=" + ctx.attr.out_package_name, + "--out=" + ctx.outputs.out.path, + ]) + ctx.actions.run( + executable = executable, + outputs = [ctx.outputs.out], + tools = tools, + arguments = arguments, + mnemonic = "PrecompileSeccomp", + toolchain = toolchain, + ) + return [DefaultInfo(files = depset([ctx.outputs.out]))] + +_run_generator = rule( + implementation = _run_generator_impl, + attrs = { + "generator": attr.label(executable = True, cfg = "target"), + "exec_generator": attr.label(executable = True, cfg = "exec"), + "out": attr.output(mandatory = True), + "out_package_name": attr.string(mandatory = True), + "qemu": attr.label(executable = True, cfg = "exec"), + "qemu_args": attr.string_list(), + }, + toolchains = target_emulator.toolchains, +) + def precompiled_seccomp_rules( name, programs_to_compile_go_library, @@ -13,7 +60,7 @@ def precompiled_seccomp_rules( """Generates a Go source file containing precompiled seccomp-bpf programs. Args: - name: Name of the final genrule. + name: Name of the generated target. programs_to_compile_go_library: go_library target which describes the set of seccomp-bpf programs that you wish to precompile. This must define the following package-level function: @@ -28,7 +75,7 @@ def precompiled_seccomp_rules( out_package_name: Go package name that `out` belongs to. exclude_in_fastbuild: Whether to skip precompilation in fastbuild mode. The auto-generated `GetPrecompiled` function will fail all lookups. - tags: List of tags to pass to the generated genrule. + tags: List of tags to pass to the generated action. """ if exclude_in_fastbuild: native.config_setting( @@ -153,69 +200,26 @@ def precompiled_seccomp_rules( ], ) - # This genrule actually runs the go_binary we just declared, and writes - # its output (containing the precompiled rules) to the desired `out` file. - # - # The generator is a source (rather than a tool) of this genrule, so that - # it is built for the target platform rather than for the execution - # platform. If the execution platform's architecture differs from the - # target's (i.e. when cross-compiling), it is run under a user-mode - # emulator. If no emulator is configured for the target architecture, it - # is run directly, which works if the kernel is configured to run foreign - # binaries (binfmt_misc) and fails otherwise. - # - # Execution platform constraints are deliberately not used here: they would - # make cross-architecture configurations fail analysis wherever no - # execution platform for the target architecture is available. - emulator = target_emulator() - out_args = " --package='" + out_package_name + "' --out=$@" - run_gen_cmd = ( - "GEN=$(location :" + name + "_gen_bin); " + - "RUN=; " + - "if [ -n \"$$TARGET_ARCH\" ] && [ \"$$(uname -m)\" != \"$$TARGET_ARCH\" ]; then " + - " RUN=\"$$EMULATOR\"; " + - "fi; " + - "$$RUN \"$$GEN\"" + out_args + " || { " + - " echo \"Failed to run $$GEN (built for $$TARGET_ARCH) on $$(uname -m)" + - " (emulator: $${RUN:-none}).\" >&2; " + - " exit 1; " + - "}" - ) - run_gen_env = ( - "TARGET_ARCH=" + select_arch( - amd64 = "x86_64", - arm64 = "aarch64", - riscv64 = "riscv64", - default = "", - ) + "; " + - "EMULATOR='" + emulator.cmd + "'; " - ) + # Compile the real generator for the target platform and the stub for the + # execution platform. The action resolves QEMU for this target/exec pair. + generator = ":" + name + "_gen_bin" + exec_generator = None if exclude_in_fastbuild: - native.genrule( - name = name, - outs = [out], - srcs = select({ - ":" + name + "_fastbuild_cond": [], - "//conditions:default": [":" + name + "_gen_bin"], - }), - # The stubbed generator's output does not depend on the - # architecture, so it is built for the execution platform. - cmd = run_gen_env + select({ - ":" + name + "_fastbuild_cond": "$(location :" + name + "_gen_stubbed_bin)" + out_args, - "//conditions:default": run_gen_cmd, - }), - tools = select({ - ":" + name + "_fastbuild_cond": [":" + name + "_gen_stubbed_bin"], - "//conditions:default": [], - }) + emulator.tools, - tags = tags + ["requires-mem:16g"], - ) - else: - native.genrule( - name = name, - outs = [out], - srcs = [":" + name + "_gen_bin"], - cmd = run_gen_env + run_gen_cmd, - tools = emulator.tools, - tags = tags + ["requires-mem:16g"], - ) + generator = select({ + ":" + name + "_fastbuild_cond": None, + "//conditions:default": generator, + }) + exec_generator = select({ + ":" + name + "_fastbuild_cond": ":" + name + "_gen_stubbed_bin", + "//conditions:default": None, + }) + _run_generator( + name = name, + generator = generator, + exec_generator = exec_generator, + out = out, + out_package_name = out_package_name, + qemu = target_emulator.binary, + qemu_args = select_arch(arm64 = ["-cpu", "neoverse-n1"], default = []), + tags = tags + ["requires-mem:16g"], + ) diff --git a/tools/bazeldefs/BUILD b/tools/bazeldefs/BUILD index a5299921398..5818f842694 100644 --- a/tools/bazeldefs/BUILD +++ b/tools/bazeldefs/BUILD @@ -1,4 +1,4 @@ -load("//tools:defs.bzl", "bzl_library", "string_flag", "target_emulators") +load("//tools:defs.bzl", "bzl_library", "string_flag") package( default_applicable_licenses = ["//:license"], @@ -35,12 +35,6 @@ bzl_library( deps = [":defs_bzl"], ) -bzl_library( - name = "deb_data_bzl", - srcs = ["extensions/deb_data.bzl"], - visibility = ["//visibility:private"], -) - # Keep Go's pure/cgo mode in its build settings, not the target platform: # Bazel 9 also uses target constraints to select a test execution platform. platform( @@ -82,10 +76,6 @@ config_setting( ], ) -# User-mode emulators for each target architecture (qemu_*). -# See target_emulator in defs.bzl. -target_emulators(name = "qemu") - genrule( name = "version", outs = ["version.txt"], diff --git a/tools/bazeldefs/defs.bzl b/tools/bazeldefs/defs.bzl index 206c89ba107..b140722b922 100644 --- a/tools/bazeldefs/defs.bzl +++ b/tools/bazeldefs/defs.bzl @@ -87,6 +87,12 @@ def amd64_config(_settings, _attr): transition_allowlist = "@bazel_tools//tools/allowlists/function_transition_allowlist" +target_emulator = struct( + toolchain_type = "@rules_qemu//qemu:toolchain_type", + toolchains = [config_common.toolchain_type("@rules_qemu//qemu:toolchain_type", mandatory = False)], + binary = None, +) + def default_installer(): return None @@ -96,71 +102,6 @@ def default_net_util(): def coreutil(): return [] # Nothing needed. -def target_emulator(): - """Returns the user-mode emulator that runs target binaries at build time. - - The emulators are pinned, statically-linked QEMU builds for the execution - platform's architecture (see target_emulators). - - Returns: - A struct with: - tools: labels to add to the tools of a genrule. - cmd: the emulator command to run target binaries with when the - execution platform's architecture differs from the target's, or an - empty string if there is none. This may reference `tools` using make - variables. - """ - return struct( - tools = select_arch( - amd64 = ["//tools/bazeldefs:qemu_x86_64"], - arm64 = ["//tools/bazeldefs:qemu_aarch64"], - riscv64 = ["//tools/bazeldefs:qemu_riscv64"], - default = [], - ), - cmd = select_arch( - amd64 = "$(execpath //tools/bazeldefs:qemu_x86_64)", - arm64 = "$(execpath //tools/bazeldefs:qemu_aarch64) -cpu neoverse-n1", - riscv64 = "$(execpath //tools/bazeldefs:qemu_riscv64)", - default = "", - ), - ) - -def target_emulators(name): - """Defines the user-mode emulators used by target_emulator. - - Each emulator is the pinned QEMU build for the execution platform's - architecture (see extensions/deb_data.bzl), or a stub that fails if there - is none. This must only be called from tools/bazeldefs/BUILD. - - Args: - name: prefix of the emulator targets, which are named _. - """ - native.alias( - name = name + "_aarch64", - actual = select({ - ":amd64": "@qemu_user_amd64_files//:usr/bin/qemu-aarch64", - "//conditions:default": ":no_emulator.sh", - }), - tags = ["manual"], - ) - native.alias( - name = name + "_riscv64", - actual = select({ - ":amd64": "@qemu_user_amd64_files//:usr/bin/qemu-riscv64", - ":arm64": "@qemu_user_arm64_files//:usr/bin/qemu-riscv64", - "//conditions:default": ":no_emulator.sh", - }), - tags = ["manual"], - ) - native.alias( - name = name + "_x86_64", - actual = select({ - ":arm64": "@qemu_user_arm64_files//:usr/bin/qemu-x86_64", - "//conditions:default": ":no_emulator.sh", - }), - tags = ["manual"], - ) - def _bpf_program_impl(ctx): resource_dir = ctx.attr._clang_resource_dir[DirectoryInfo] kernel_headers = ctx.attr._kernel_headers[DirectoryInfo] diff --git a/tools/bazeldefs/extensions/deb_data.bzl b/tools/bazeldefs/extensions/deb_data.bzl deleted file mode 100644 index 853e9ebb831..00000000000 --- a/tools/bazeldefs/extensions/deb_data.bzl +++ /dev/null @@ -1,22 +0,0 @@ -"""Repository rule that extracts the data archive of a Debian package.""" - -def _deb_data_impl(rctx): - rctx.extract(archive = rctx.path(rctx.attr.data)) - rctx.file("BUILD.bazel", rctx.attr.build_file_content) - -deb_data = repository_rule( - implementation = _deb_data_impl, - attrs = { - "build_file_content": attr.string( - doc = "Content of the BUILD file of the repository.", - mandatory = True, - ), - "data": attr.label( - doc = "The data archive (e.g. data.tar.xz) of a Debian package, " + - "as unpacked by http_archive(type = \"deb\").", - allow_single_file = True, - mandatory = True, - ), - }, - doc = """Extracts the files installed by a Debian package.""", -) diff --git a/tools/bazeldefs/no_emulator.sh b/tools/bazeldefs/no_emulator.sh deleted file mode 100755 index 3abb22171fb..00000000000 --- a/tools/bazeldefs/no_emulator.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -# Copyright 2026 The gVisor Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -echo "$0: no user-mode emulator is available for $(uname -m) build machines" >&2 -exit 1 diff --git a/tools/defs.bzl b/tools/defs.bzl index c04266e8064..1c2a815e854 100644 --- a/tools/defs.bzl +++ b/tools/defs.bzl @@ -7,7 +7,7 @@ change for Google-internal and bazel-compatible rules. load("//tools:arch.bzl", "arch_genrule", "arch_transition_impl") load("//tools/bazeldefs:cc.bzl", _cc_binary = "cc_binary", _cc_flags_supplier = "cc_flags_supplier", _cc_grpc_library = "cc_grpc_library", _cc_library = "cc_library", _cc_proto_library = "cc_proto_library", _cc_test = "cc_test", _cc_toolchain = "cc_toolchain", _gbenchmark = "gbenchmark", _gbenchmark_internal = "gbenchmark_internal", _grpcpp = "grpcpp", _gtest = "gtest", _select_gtest = "select_gtest") -load("//tools/bazeldefs:defs.bzl", _BuildSettingInfo = "BuildSettingInfo", _bool_flag = "bool_flag", _bpf_program = "bpf_program", _build_test = "build_test", _bzl_library = "bzl_library", _coreutil = "coreutil", _default_net_util = "default_net_util", _expand_template = "expand_template", _more_shards = "more_shards", _most_shards = "most_shards", _proto_library = "proto_library", _select_arch = "select_arch", _select_system = "select_system", _short_path = "short_path", _string_flag = "string_flag", _target_emulator = "target_emulator", _target_emulators = "target_emulators", _version = "version") +load("//tools/bazeldefs:defs.bzl", _BuildSettingInfo = "BuildSettingInfo", _bool_flag = "bool_flag", _bpf_program = "bpf_program", _build_test = "build_test", _bzl_library = "bzl_library", _coreutil = "coreutil", _default_net_util = "default_net_util", _expand_template = "expand_template", _more_shards = "more_shards", _most_shards = "most_shards", _proto_library = "proto_library", _select_arch = "select_arch", _select_system = "select_system", _short_path = "short_path", _string_flag = "string_flag", _target_emulator = "target_emulator", _version = "version") load("//tools/bazeldefs:go.bzl", _cov_available = "cov_available", _gazelle = "gazelle", _go_binary = "go_binary", _go_binary_nogo_dep = "go_binary_nogo_dep", _go_cov = "go_cov", _go_grpc_and_proto_libraries = "go_grpc_and_proto_libraries", _go_imports = "go_imports", _go_library = "go_library", _go_path = "go_path", _go_proto_library = "go_proto_library", _go_reset_target = "go_reset_target", _go_test = "go_test", _gotsan_flag_values = "gotsan_flag_values", _gotsan_values = "gotsan_values", _select_goarch = "select_goarch", _select_goos = "select_goos") load("//tools/bazeldefs:pkg.bzl", _pkg_deb = "pkg_deb", _pkg_tar = "pkg_tar") load("//tools/bazeldefs:platforms.bzl", _default_platform = "default_platform", _platform_capabilities = "platform_capabilities", _platforms = "platforms", _save_restore_platforms = "save_restore_platforms") @@ -29,7 +29,6 @@ select_arch = _select_arch select_system = _select_system short_path = _short_path target_emulator = _target_emulator -target_emulators = _target_emulators coreutil = _coreutil more_shards = _more_shards most_shards = _most_shards diff --git a/tools/licensecheck/dependencies.yaml b/tools/licensecheck/dependencies.yaml index f7a8291fcba..6726240f0b7 100644 --- a/tools/licensecheck/dependencies.yaml +++ b/tools/licensecheck/dependencies.yaml @@ -4,6 +4,8 @@ # Check completeness with ARGS=--mode=verify. # Entries whose license cannot be fetched automatically are maintained by hand and preserved # by --mode=fetch, so you can hand-edit the file for these. +# license_source selects an immutable upstream notice for a pinned binary artifact; +# changing its artifact URL/hash requires renewing that explicit source audit. - dependency: abseil-cpp version: https://github.com/abseil/abseil-cpp/releases/download/20260817.0/abseil-cpp-20260817.0.tar.gz retrieved: "2026-10-06" @@ -1151,18 +1153,146 @@ commit: 4eb9011f3e6d551d067d87c89f082261164fac31 sha256: 26dc6db523ee943af51fdfc292658a2dbe7046735654f1dc839745988f0fe26b license: Apache-2.0 -- dependency: qemu_user_amd64 - version: https://snapshot.debian.org/archive/debian/20260905T205002Z/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_amd64.deb - retrieved: "2026-10-05" - commit: "" - sha256: ca6ede739327a20ae5a3498230c8a3a9a072c586e131bc6bcc1201eb1725e336 +- dependency: qemu_user_prebuilt_linux_amd64_aarch64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-aarch64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: cc3bf42da8f219cd314a7806053c73cca166bf14c288daf5b6105e66590a6dee license: GPL-2.0-only -- dependency: qemu_user_arm64 - version: https://snapshot.debian.org/archive/debian/20260905T205002Z/pool/main/q/qemu/qemu-user_10.0.13%2Bds-0%2Bdeb13u1_arm64.deb - retrieved: "2026-10-05" - commit: "" - sha256: c73711af02b97cd5e2667e735d9c06890c57165517110ca4e646c95a7083158d + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_arm + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-arm-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 7efa7a2dfb1a325dd514e16b67d38a6ead24b0bd5326ac0b1b8e8b6667c01698 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_i386 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-i386-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 60cc85a81c4cea6072230211b0cc1742557a6d57f53638d03d8d3c655c366872 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_mips64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-mips64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 1f7d80af0b340b9ad7dec24e6ea01d7028a60e55ebfa1d7ac61a5418f4127621 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_ppc + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-ppc-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: cd90ee01c78cfe2065195076dc9f866cff91cc7aa39c07961eb4f82bbf9eec2a + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_ppc64le + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-ppc64le-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 21b1474ff3e9f05f2abca931659d70506000dd9413fb8662444f4ad739e3aa63 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_riscv32 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-riscv32-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 1d52e2a9e710a39eb7a7e0a6ee1a4b4a964625512cc75ebcd1e1b18c8678367b + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_riscv64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-riscv64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: b974c689b39e704696eed04500e9a2da4105f0b643e5119d01ef4826d6c7f0a0 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_s390x + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-s390x-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 6b0cc9d4afee60b5d5f960622aa77c926b80366f7454d027cf5c0c2e29712e86 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_amd64_x86_64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-amd64-x86_64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 911d8c4dd7ca3780fe11adfa8db52a397e319b4feb46398b2e7488590c8040b6 license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_aarch64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-aarch64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: fa6a9e126306ab3f06f718473425d6094b7336cac7d21a9328a36af673e5d0a4 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_arm + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-arm-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 56220cf250f745253c05f86d55dcccf2d4bcab61840e744a12bda781f735b035 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_i386 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-i386-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 7400ebdfa9020e86847320d0f412906afb0d634e09938792b36340aa0ecf67be + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_mips64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-mips64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 1d9aeb6c36c2cb23ffd8169393dc7de83122664a2d508d27f56a3369c49e413a + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_ppc + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-ppc-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 0572c14bf243f4c05aaa0170fea140a4763db2495cede7ec41f705ebf5981e3e + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_ppc64le + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-ppc64le-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: ab0cb3154f6fa2d6ad91f300e41af82e35b6f9722d99d28ddb787f31dbe399a0 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_riscv32 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-riscv32-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 1076332ae82dfac4102778c5aedbd45eed5f0386038812328855e7d802d148f4 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_riscv64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-riscv64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 463545d675e5df5be0cc493c6822cd00c8708f8bff0b98e6d4e5ad1dcb169e39 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_s390x + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-s390x-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: d864b9e065e41495b593fd0d5038e000f7e32b1a47eff3ae6d8c54a8c238e76d + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING +- dependency: qemu_user_prebuilt_linux_arm64_x86_64 + version: https://github.com/hermeticbuild/qemu-prebuilt/releases/download/11.0.0.1/qemu-user-linux-arm64-x86_64-11.0.0.1.tar.zst + retrieved: "2026-10-17" + commit: 98b060da3a4f92b2a994ead5b16a87e783baf77c + sha256: 58d31d097b72207702458bcd5927337e3428bad3f97d3075ff6c7b4ca13ecf50 + license: GPL-2.0-only + license_source: https://raw.githubusercontent.com/qemu/qemu/98b060da3a4f92b2a994ead5b16a87e783baf77c/COPYING - dependency: rules_cc version: https://github.com/bazelbuild/rules_cc/releases/download/0.2.25/rules_cc-0.2.25.tar.gz retrieved: "2026-10-02" @@ -1199,6 +1329,12 @@ commit: 6aad8828e82667cda09ff449862a5a8559bee27b sha256: 2119ca04726066c53b8f1ff42dffa090ea7f3e42a75679f1cfda937406cf7753 license: Apache-2.0 +- dependency: rules_qemu + version: https://github.com/hermeticbuild/rules_qemu/releases/download/v0.3.0/rules_qemu-v0.3.0.tar.gz + retrieved: "2026-10-17" + commit: de0cdb392c5a4095528122a0725d48eec9c6befd + sha256: cc9ee54a94849eb2467d7ba71773a28ee53f14a0a1d615f283a770d532df889b + license: Apache-2.0 - dependency: rules_shell version: https://github.com/bazel-contrib/rules_shell/releases/download/v0.8.0/rules_shell-v0.8.0.tar.gz retrieved: "2026-09-24" diff --git a/tools/licensecheck/licensecheck.go b/tools/licensecheck/licensecheck.go index 686d39b3fba..4accc7e7f0f 100644 --- a/tools/licensecheck/licensecheck.go +++ b/tools/licensecheck/licensecheck.go @@ -18,14 +18,16 @@ // mod` in the dockerized build environment): the root module's direct // bazel_deps, the http_archive/http_file repos declared in MODULE.bazel, and // the Go repos imported from the go_deps extension, unioned with go.mod's -// requirements. Fetch downloads each dependency's license text, from the Go -// module proxy for Go modules and from GitHub for everything else, classifies +// requirements, plus the user-mode archives exposed by the QEMU toolchain. +// Fetch downloads each dependency's license text from the Go module proxy +// for Go modules and from GitHub for everything else, classifies // it, and records it in a YAML file. Verify checks that the YAML file has an // entry for every dependency, without fetching any licenses. // // Entries whose license cannot be fetched automatically (e.g. // @google_root_pem) are maintained by hand: Fetch preserves an existing entry -// whenever fetching fails. +// whenever fetching fails. Explicit license_source notices are re-fetched on +// every audit and fail closed if the notice or its artifact binding is invalid. package licensecheck import ( @@ -70,6 +72,9 @@ type Entry struct { Commit string `yaml:"commit"` SHA256 string `yaml:"sha256"` License Licenses `yaml:"license"` + // LicenseSource pins the notice for an executable distributed by a + // differently licensed builder repository. It is bound to Version/SHA256. + LicenseSource string `yaml:"license_source,omitempty"` } // License is an SPDX license identifier (https://spdx.org/licenses/), one of @@ -201,6 +206,8 @@ const ( # Check completeness with ARGS=--mode=verify. # Entries whose license cannot be fetched automatically are maintained by hand and preserved # by --mode=fetch, so you can hand-edit the file for these. +# license_source selects an immutable upstream notice for a pinned binary artifact; +# changing its artifact URL/hash requires renewing that explicit source audit. ` dateFormat = "2006-01-02" fetchWorkers = 8 @@ -234,7 +241,7 @@ func Fetch(p Paths) error { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() - f, err := fetchLicense(d) + f, err := fetchLicense(d, oldByName[d.name]) results[i] = result{f, err} }(i, d) } @@ -246,7 +253,11 @@ func Fetch(p Paths) error { for i, d := range deps { r := results[i] if r.err != nil { - if e, ok := oldByName[d.name]; ok && len(e.License) > 0 { + if e := oldByName[d.name]; e.LicenseSource != "" { + // Keep the explicit source so a later audit cannot silently + // fall back to the builder repository's license. + entries = append(entries, e) + } else if e, ok := oldByName[d.name]; ok && len(e.License) > 0 { fmt.Fprintf(os.Stderr, "%-60s %s (kept existing entry: %v)\n", d.name, e.License, r.err) entries = append(entries, e) continue @@ -256,12 +267,13 @@ func Fetch(p Paths) error { continue } e := Entry{ - Dependency: d.name, - Version: d.source(), - Retrieved: today, - Commit: r.fetched.commit, - SHA256: r.fetched.sha256, - License: r.fetched.license, + Dependency: d.name, + Version: d.source(), + Retrieved: today, + Commit: r.fetched.commit, + SHA256: r.fetched.sha256, + License: r.fetched.license, + LicenseSource: oldByName[d.name].LicenseSource, } if o, ok := oldByName[d.name]; ok && o.Version == e.Version && o.SHA256 == e.SHA256 { // Commit lookup is best-effort. A missing response does not @@ -335,6 +347,11 @@ func verifyProblems(deps []dep, entries []Entry) []string { case len(e.License) == 0: problems = append(problems, fmt.Sprintf("%s has no license", d.name)) default: + if commit, err := licenseSourceCommit(d, e); err != nil { + problems = append(problems, fmt.Sprintf("%s: %v", d.name, err)) + } else if commit != "" && commit != e.Commit { + problems = append(problems, fmt.Sprintf("%s: license_source commit does not match the audited commit", d.name)) + } if e.Version != d.source() { problems = append(problems, fmt.Sprintf("%s was audited at %q, but is now %q", d.name, e.Version, d.source())) } @@ -365,6 +382,11 @@ func verifyProblems(deps []dep, entries []Entry) []string { return problems } +// repoRef identifies a repository to audit and its stable catalog name. +type repoRef struct { + ref, name string +} + // enumerate returns all external dependencies, sorted by name. func enumerate(p Paths) ([]dep, error) { graphOut, err := makeMod("graph", "--depth", "1", "--extension_info=all", "--output", "json") @@ -376,10 +398,7 @@ func enumerate(p Paths) ([]dep, error) { return nil, err } // refs are the repos to audit: modules as name@version, everything else - // by apparent repo name. - type repoRef struct { - ref, name string - } + // by apparent or canonical repo name. var refs []repoRef for _, m := range graph.Dependencies { refs = append(refs, repoRef{m.Key, m.Name}) @@ -391,6 +410,27 @@ func enumerate(p Paths) ([]dep, error) { // http_archive/http_file declared directly in MODULE.bazel. case strings.HasSuffix(u.Key, "%go_deps"): // Go repos imported from the gazelle go_deps extension. + case strings.HasSuffix(u.Key, "//qemu/extension:qemu.bzl%qemu"): + // The public user toolchain refers to indirect archive repos. Audit + // its complete user-mode family, including unselected architectures. + root, err := readRepoMapping("") + if err != nil { + return nil, err + } + hub := root["qemu_user_toolchains"] + if hub == "" { + return nil, errors.New("QEMU user toolchain repository is not imported") + } + mapping, err := readRepoMapping(hub) + if err != nil { + return nil, err + } + qemuRefs, err := qemuUserRepoRefs(mapping) + if err != nil { + return nil, err + } + refs = append(refs, qemuRefs...) + continue default: // Toolchain extensions (crosstool, llvm_zlib, python, go_sdk, // ...) are not audited. @@ -469,8 +509,47 @@ func enumerate(p Paths) ([]dep, error) { return deps, nil } +// readRepoMapping asks Bazel for apparent-to-canonical repository names. +func readRepoMapping(repo string) (map[string]string, error) { + if repo == "" { + // Preserve the empty root repository argument through make's shell. + repo = "''" + } + out, err := makeMod("dump_repo_mapping", repo) + if err != nil { + return nil, err + } + start := strings.Index(out, "{") + if start < 0 { + return nil, fmt.Errorf("no repository mapping JSON in %q", out) + } + var mapping map[string]string + if err := json.Unmarshal([]byte(out[start:]), &mapping); err != nil { + return nil, fmt.Errorf("cannot parse repository mapping: %w", err) + } + return mapping, nil +} + +func qemuUserRepoRefs(mapping map[string]string) ([]repoRef, error) { + var refs []repoRef + for name, canonical := range mapping { + if !strings.HasPrefix(name, "qemu_user_prebuilt_") { + continue + } + if canonical == "" { + return nil, fmt.Errorf("QEMU archive %s has no canonical repository name", name) + } + refs = append(refs, repoRef{"@@" + canonical, name}) + } + if len(refs) == 0 { + return nil, errors.New("QEMU user toolchain exposes no emulator archives") + } + slices.SortFunc(refs, func(a, b repoRef) int { return strings.Compare(a.name, b.name) }) + return refs, nil +} + // makeMod runs `make mod TARGETS="..."`, which wraps `bazel mod`. -func makeMod(args ...string) (string, error) { +var makeMod = func(args ...string) (string, error) { cmd := exec.Command("make", "-s", "mod", "OPTIONS=", "TARGETS="+strings.Join(args, " ")) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout @@ -645,7 +724,22 @@ type fetched struct { // fetchLicense returns the upstream commit, artifact hash, and licenses of a // dependency. -func fetchLicense(d dep) (*fetched, error) { +func fetchLicense(d dep, previous Entry) (*fetched, error) { + commit, err := licenseSourceCommit(d, previous) + if err != nil { + return nil, err + } + if commit != "" { + body, err := httpGet(previous.LicenseSource, nil) + if err != nil { + return nil, err + } + licenses, err := classify(string(body)) + if err != nil { + return nil, err + } + return &fetched{commit: commit, sha256: d.sha256, license: licenses}, nil + } if d.kind == kindGoModule { return fetchGoModule(d.name, d.version) } @@ -657,6 +751,29 @@ func fetchLicense(d dep) (*fetched, error) { return f, nil } +var artifactSHA256RE = regexp.MustCompile(`^[0-9a-f]{64}$`) + +// licenseSourceCommit requires immutable upstream notice bytes, not a mutable +// tag or the repository that happens to distribute the executable. +func licenseSourceCommit(d dep, e Entry) (string, error) { + if e.LicenseSource == "" { + return "", nil + } + if d.kind != kindArchive || !artifactSHA256RE.MatchString(d.sha256) || e.Version != d.source() || e.SHA256 != d.sha256 { + return "", errors.New("license_source artifact identity changed or is unpinned; audit the new artifact and refresh its notice source") + } + owner, repo, commit, err := parseGitHubURL(e.LicenseSource) + if err != nil || !commitRE.MatchString(commit) { + return "", errors.New("license_source must identify a GitHub notice pinned to a full commit") + } + prefix := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/", owner, repo, commit) + name, ok := strings.CutPrefix(e.LicenseSource, prefix) + if !ok || !slices.Contains(licenseFileNames, name) { + return "", errors.New("license_source must name a recognized raw GitHub root notice") + } + return commit, nil +} + // licenseFileNames are candidate root paths for GitHub raw-file requests. // Module ZIPs provide a directory listing and discover suffixed notices below. var licenseFileNames = []string{ diff --git a/tools/licensecheck/licensecheck_test.go b/tools/licensecheck/licensecheck_test.go index 71c9d2c1e3d..14160fcbcb3 100644 --- a/tools/licensecheck/licensecheck_test.go +++ b/tools/licensecheck/licensecheck_test.go @@ -17,7 +17,10 @@ package licensecheck import ( "bytes" _ "embed" + "errors" + "io" "io/fs" + "net/http" "os" "path/filepath" "slices" @@ -413,3 +416,292 @@ func TestCheckPolicy(t *testing.T) { t.Errorf("CheckPolicy returned %d problems, want 5: %v", len(problems), problems) } } + +func TestLicenseSourceIdentity(t *testing.T) { + const artifact = "https://github.com/example/builder/releases/download/v1/tool.tar.gz" + const commit = "0123456789abcdef0123456789abcdef01234567" + const source = "https://raw.githubusercontent.com/example/upstream/" + commit + "/COPYING" + const hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + for _, test := range []struct { + name string + url string + hash string + source string + goModule bool + wantErr bool + }{ + {name: "pinned", url: artifact, hash: hash, source: source}, + {name: "automatic", url: artifact, hash: hash}, + {name: "changed artifact", url: artifact + "?changed", hash: hash, source: source, wantErr: true}, + {name: "changed digest", url: artifact, hash: strings.Repeat("a", 64), source: source, wantErr: true}, + {name: "unpinned artifact", url: artifact, source: source, wantErr: true}, + {name: "mutable notice", url: artifact, hash: hash, source: strings.ReplaceAll(source, commit, "main"), wantErr: true}, + {name: "notice traversal", url: artifact, hash: hash, source: strings.ReplaceAll(source, "/COPYING", "/../main/COPYING"), wantErr: true}, + {name: "encoded traversal", url: artifact, hash: hash, source: strings.ReplaceAll(source, "/COPYING", "/%2e%2e/main/COPYING"), wantErr: true}, + {name: "go module", url: artifact, hash: hash, source: source, goModule: true, wantErr: true}, + } { + t.Run(test.name, func(t *testing.T) { + d := dep{name: "tool", kind: kindArchive, url: test.url, sha256: test.hash} + if test.goModule { + d.kind = kindGoModule + } + e := Entry{Version: artifact, SHA256: hash, LicenseSource: test.source} + got, err := licenseSourceCommit(d, e) + if (err != nil) != test.wantErr { + t.Fatalf("licenseSourceCommit = %q, %v; want error %t", got, err, test.wantErr) + } + if !test.wantErr && test.source != "" && got != commit { + t.Fatalf("licenseSourceCommit = %q, want %q", got, commit) + } + }) + } +} + +func TestVerifyLicenseSource(t *testing.T) { + const commit = "0123456789abcdef0123456789abcdef01234567" + d := dep{name: "tool", kind: kindArchive, url: "https://example.com/tool.tar.gz", sha256: strings.Repeat("a", 64)} + e := Entry{Dependency: d.name, Version: d.url, SHA256: d.sha256, Retrieved: "2026-10-17", Commit: commit, License: Licenses{gpl2}, LicenseSource: "https://raw.githubusercontent.com/example/upstream/" + commit + "/COPYING"} + if problems := verifyProblems([]dep{d}, []Entry{e}); len(problems) != 0 { + t.Fatalf("verifyProblems = %v, want a valid explicit notice", problems) + } + e.Commit = strings.Repeat("b", 40) + if problems := verifyProblems([]dep{d}, []Entry{e}); len(problems) != 1 || !strings.Contains(problems[0], "license_source commit") { + t.Fatalf("verifyProblems = %v, want mismatched notice commit", problems) + } + e.LicenseSource = "https://raw.githubusercontent.com/example/upstream/main/COPYING" + if problems := verifyProblems([]dep{d}, []Entry{e}); len(problems) != 1 || !strings.Contains(problems[0], "pinned to a full commit") { + t.Fatalf("verifyProblems = %v, want unpinned notice error", problems) + } +} + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestReadRepoMapping(t *testing.T) { + origMakeMod := makeMod + defer func() { makeMod = origMakeMod }() + + makeMod = func(args ...string) (string, error) { + return "", errors.New("boom") + } + if _, err := readRepoMapping(""); err == nil { + t.Fatal("readRepoMapping succeeded on makeMod error") + } + + makeMod = func(args ...string) (string, error) { + return "no json here", nil + } + if _, err := readRepoMapping("hub"); err == nil || !strings.Contains(err.Error(), "no repository mapping JSON") { + t.Fatalf("readRepoMapping = %v, want missing JSON error", err) + } + + makeMod = func(args ...string) (string, error) { + return "{invalid json", nil + } + if _, err := readRepoMapping("hub"); err == nil || !strings.Contains(err.Error(), "cannot parse repository mapping") { + t.Fatalf("readRepoMapping = %v, want parse error", err) + } +} + +func TestEnumerateAndFetchQemuLicenseSource(t *testing.T) { + origMakeMod := makeMod + origHTTPClient := httpClient + defer func() { + makeMod = origMakeMod + httpClient = origHTTPClient + }() + + const ( + depName = "qemu_user_prebuilt_linux_amd64_aarch64" + artifact = "https://github.com/hermeticbuild/rules_qemu/releases/download/v0.3.0/qemu.tar.zst" + sha = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + commit = "0123456789abcdef0123456789abcdef01234567" + source = "https://raw.githubusercontent.com/qemu/qemu/" + commit + "/COPYING" + gpl2Text = "GNU GENERAL PUBLIC LICENSE Version 2, June 1991" + ) + + dir := t.TempDir() + goModPath := filepath.Join(dir, "go.mod") + if err := os.WriteFile(goModPath, []byte("module gvisor.dev/gvisor\n\ngo 1.23\n"), 0644); err != nil { + t.Fatal(err) + } + yamlPath := filepath.Join(dir, "dependencies.yaml") + paths := Paths{GoMod: goModPath, YAML: yamlPath} + + graphJSON := `{"dependencies":[],"extensionUsages":[{"key":"@@rules_qemu+//qemu/extension:qemu.bzl%qemu","used_repos":["qemu_user_toolchains"]}]}` + showRepoOut := "## @@rules_qemu++qemu+arm:\nhttp_archive(\n urls = [\"" + artifact + "\"],\n sha256 = \"" + sha + "\",\n)\n" + + for _, tc := range []struct { + name string + rootMap string + rootErr error + hubMap string + hubErr error + wantErr string + }{ + { + name: "root mapping error", + rootErr: errors.New("root dump failed"), + wantErr: "root dump failed", + }, + { + name: "missing hub", + rootMap: `{}`, + wantErr: "QEMU user toolchain repository is not imported", + }, + { + name: "hub mapping error", + rootMap: `{"qemu_user_toolchains":"rules_qemu++qemu+hub"}`, + hubErr: errors.New("hub dump failed"), + wantErr: "hub dump failed", + }, + { + name: "empty user archives", + rootMap: `{"qemu_user_toolchains":"rules_qemu++qemu+hub"}`, + hubMap: `{"qemu_system_data_prebuilt_linux_amd64":"rules_qemu++qemu+fw"}`, + wantErr: "exposes no emulator archives", + }, + } { + t.Run(tc.name, func(t *testing.T) { + makeMod = func(args ...string) (string, error) { + switch args[0] { + case "graph": + return graphJSON, nil + case "dump_repo_mapping": + if args[1] == "''" { + return tc.rootMap, tc.rootErr + } + return tc.hubMap, tc.hubErr + default: + return showRepoOut, nil + } + } + if _, err := enumerate(paths); err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("enumerate() = %v, want error containing %q", err, tc.wantErr) + } + }) + } + + makeMod = func(args ...string) (string, error) { + switch args[0] { + case "graph": + return graphJSON, nil + case "dump_repo_mapping": + if args[1] == "''" { + return `{"qemu_user_toolchains":"rules_qemu++qemu+hub"}`, nil + } + return `{"` + depName + `":"rules_qemu++qemu+arm"}`, nil + case "show_repo": + return showRepoOut, nil + default: + return "", errors.New("unexpected makeMod call") + } + } + + seed := []Entry{{ + Dependency: depName, + Version: artifact, + Retrieved: "2026-10-17", + Commit: commit, + SHA256: sha, + License: Licenses{gpl2}, + LicenseSource: source, + }} + if err := writeEntries(yamlPath, seed); err != nil { + t.Fatal(err) + } + + // Successful Fetch preserves LicenseSource and classifies the upstream notice. + httpClient = &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.String() != source { + return &http.Response{StatusCode: http.StatusNotFound, Body: io.NopCloser(strings.NewReader(""))}, nil + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(gpl2Text)), + }, nil + }), + } + if err := Fetch(paths); err != nil { + t.Fatalf("Fetch() = %v, want nil", err) + } + got, err := readEntries(yamlPath) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].LicenseSource != source || !slices.Equal(got[0].License, Licenses{gpl2}) { + t.Fatalf("readEntries after Fetch = %+v, want preserved LicenseSource and GPL-2.0-only", got) + } + + // Failing notice classification fails Fetch while keeping the explicit entry on disk. + httpClient = &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("unrecognized license text")), + }, nil + }), + } + if err := Fetch(paths); err == nil { + t.Fatal("Fetch() succeeded on unclassified notice, want error") + } + got, err = readEntries(yamlPath) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].LicenseSource != source { + t.Fatalf("readEntries after failed Fetch = %+v, want preserved explicit entry", got) + } + + // HTTP failure on explicit notice also fails closed. + httpClient = &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusNotFound, Body: io.NopCloser(strings.NewReader(""))}, nil + }), + } + if _, err := fetchLicense(dep{name: depName, kind: kindArchive, url: artifact, sha256: sha}, seed[0]); err == nil { + t.Fatal("fetchLicense() succeeded on 404 notice, want error") + } + // Invalid license_source fails before HTTP. + badEntry := seed[0] + badEntry.LicenseSource = "https://example.com/bad" + if _, err := fetchLicense(dep{name: depName, kind: kindArchive, url: artifact, sha256: sha}, badEntry); err == nil { + t.Fatal("fetchLicense() succeeded on invalid license_source, want error") + } +} + +func TestQemuUserRepoRefs(t *testing.T) { + // A public hub maps indirect repos that the root module cannot import by + // apparent name. Firmware and system tools are outside this user-mode audit. + mapping := map[string]string{ + "qemu_user_prebuilt_linux_amd64_aarch64": "rules_qemu++qemu+arm", + "qemu_user_prebuilt_linux_arm64_x86_64": "rules_qemu++qemu+x86", + "qemu_user_toolchains": "rules_qemu++qemu+hub", + "qemu_system_data_prebuilt_linux_amd64": "rules_qemu++qemu+firmware", + "qemu_img_prebuilt_linux_amd64": "rules_qemu++qemu+img", + } + got, err := qemuUserRepoRefs(mapping) + if err != nil { + t.Fatal(err) + } + want := []repoRef{ + {"@@rules_qemu++qemu+arm", "qemu_user_prebuilt_linux_amd64_aarch64"}, + {"@@rules_qemu++qemu+x86", "qemu_user_prebuilt_linux_arm64_x86_64"}, + } + if !slices.Equal(got, want) { + t.Errorf("qemuUserRepoRefs() = %v, want %v", got, want) + } + for _, invalid := range []map[string]string{ + {}, + {"qemu_user_toolchains": "rules_qemu++qemu+hub"}, + {"qemu_user_prebuilt_linux_amd64_aarch64": ""}, + } { + if _, err := qemuUserRepoRefs(invalid); err == nil { + t.Errorf("qemuUserRepoRefs(%v) succeeded, want missing archive identity error", invalid) + } + } +}