From e9b91f534d1fd712f925394530f4cc33793fce9a Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Mon, 1 Jul 2024 14:15:55 +0100 Subject: [PATCH] fix: make Python repositories `chmod`/`id`/`touch` hermetic --- python/private/chmod.py | 77 +++++++++++++++++++++++++++++++++++++++++ python/private/id.py | 45 ++++++++++++++++++++++++ python/private/touch.py | 43 +++++++++++++++++++++++ python/repositories.bzl | 65 +++++++++++++++++++--------------- 4 files changed, 203 insertions(+), 27 deletions(-) create mode 100755 python/private/chmod.py create mode 100755 python/private/id.py create mode 100755 python/private/touch.py diff --git a/python/private/chmod.py b/python/private/chmod.py new file mode 100755 index 0000000000..05d2a21a79 --- /dev/null +++ b/python/private/chmod.py @@ -0,0 +1,77 @@ +#! /usr/bin/env python3 + +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# 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. + +""" +Provides a `chmod` implementation that recursively removes write permissions. +""" + +from __future__ import annotations + +from itertools import chain +from argparse import ArgumentParser +from pathlib import Path +from stat import S_IWGRP, S_IWOTH, S_IWUSR + + +def readonly(value: str) -> int: + if value != "ugo-w": + raise ValueError("Only `ugo-w` is supported") + + return ~(S_IWUSR | S_IWGRP | S_IWOTH) + + +def directory(value: str) -> Path: + path = Path(value) + + if not path.exists(): + raise ValueError(f"`{path}` must exist") + + if not path.is_dir(): + raise ValueError("Must be a directory") + + return path + + +def main(): + parser = ArgumentParser(prog="chmod", description="Change file mode bits.") + parser.add_argument( + "-R", + "--recursive", + action="store_true", + help="Recursively set permissions.", + required=True, + ) + parser.add_argument( + "mask", + metavar="MODE", + help="Symbolic mode settings.", + type=readonly, + ) + parser.add_argument( + "directory", + metavar="FILE", + help="Filepath(s) to operate on.", + type=directory, + ) + args = parser.parse_args() + + for path in chain((args.directory,), args.directory.glob("**/*")): + stat = path.stat() + path.chmod(stat.st_mode & args.mask) + + +if __name__ == "__main__": + main() diff --git a/python/private/id.py b/python/private/id.py new file mode 100755 index 0000000000..8531e56deb --- /dev/null +++ b/python/private/id.py @@ -0,0 +1,45 @@ +#! /usr/bin/env python3 + +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# 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. + +""" +Provides an `id` implementation. +""" + +from __future__ import annotations + +from argparse import ArgumentParser +from os import getuid + + +def main(): + parser = ArgumentParser( + prog="id", description="Print real and effective user and group IDs." + ) + parser.add_argument( + "-u", + "--user", + help="Print only the effective user ID.", + action="store_true", + required=True, + ) + args = parser.parse_args() + assert args.user + + print(getuid()) + + +if __name__ == "__main__": + main() diff --git a/python/private/touch.py b/python/private/touch.py new file mode 100755 index 0000000000..b9879d2e38 --- /dev/null +++ b/python/private/touch.py @@ -0,0 +1,43 @@ +#! /usr/bin/env python3 + +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# 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. + +""" +Provides a `touch` implementation. +""" + +from __future__ import annotations + +from argparse import ArgumentParser +from pathlib import Path + + +def main(): + parser = ArgumentParser(prog="touch", description="Change file timestamps.") + parser.add_argument( + "files", + metavar="FILE", + help="Filepath(s) to operate on.", + type=Path, + nargs="+", + ) + args = parser.parse_args() + + for path in args.files: + path.touch() + + +if __name__ == "__main__": + main() diff --git a/python/repositories.bzl b/python/repositories.bzl index d58feefd31..9b17967073 100644 --- a/python/repositories.bzl +++ b/python/repositories.bzl @@ -27,6 +27,8 @@ load("//python/private:internal_config_repo.bzl", "internal_config_repo") load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load( "//python/private:toolchains_repo.bzl", + "get_host_os_arch", + "get_host_platform", "host_toolchain", "multi_toolchain_aliases", "toolchain_aliases", @@ -110,7 +112,10 @@ def _python_repository_impl(rctx): if bool(rctx.attr.url) == bool(rctx.attr.urls): fail("Exactly one of (url, urls) must be set.") + (os_name, arch) = get_host_os_arch(rctx) + host_platform = get_host_platform(os_name, arch) platform = rctx.attr.platform + python_bin = "python.exe" if ("windows" in platform) else "bin/python3" python_version = rctx.attr.python_version python_version_info = python_version.split(".") python_short_version = "{0}.{1}".format(*python_version_info) @@ -189,36 +194,33 @@ def _python_repository_impl(rctx): # pycs being generated at runtime: # * The pycs are not deterministic (they contain timestamps) # * Multiple processes trying to write the same pycs can result in errors. - if not rctx.attr.ignore_root_user_error: - if "windows" not in platform: - lib_dir = "lib" if "windows" not in platform else "Lib" + if not rctx.attr.ignore_root_user_error and host_platform == platform: + lib_dir = "lib" if "windows" not in platform else "Lib" - repo_utils.execute_checked( - rctx, - op = "python_repository.MakeReadOnly", - arguments = [repo_utils.which_checked(rctx, "chmod"), "-R", "ugo-w", lib_dir], - ) - exec_result = repo_utils.execute_unchecked( + repo_utils.execute_checked( + rctx, + op = "python_repository.MakeReadOnly", + arguments = [python_bin, "-B", rctx.attr._chmod, "-R", "ugo-w", lib_dir], + ) + exec_result = repo_utils.execute_unchecked( + rctx, + op = "python_repository.TestReadOnly", + arguments = [python_bin, rctx.attr._touch, "{}/.test".format(lib_dir)], + ) + + # The issue with running as root is the installation is no longer + # read-only, so the problems due to pyc can resurface. + if exec_result.return_code == 0 and "windows" not in platform: + stdout = repo_utils.execute_checked_stdout( rctx, - op = "python_repository.TestReadOnly", - arguments = [repo_utils.which_checked(rctx, "touch"), "{}/.test".format(lib_dir)], + op = "python_repository.GetUserId", + arguments = [python_bin, rctx.attr._id, "-u"], ) - - # The issue with running as root is the installation is no longer - # read-only, so the problems due to pyc can resurface. - if exec_result.return_code == 0: - stdout = repo_utils.execute_checked_stdout( - rctx, - op = "python_repository.GetUserId", - arguments = [repo_utils.which_checked(rctx, "id"), "-u"], - ) - uid = int(stdout.strip()) - if uid == 0: - fail("The current user is root, please run as non-root when using the hermetic Python interpreter. See https://github.com/bazelbuild/rules_python/pull/713.") - else: - fail("The current user has CAP_DAC_OVERRIDE set, please drop this capability when using the hermetic Python interpreter. See https://github.com/bazelbuild/rules_python/pull/713.") - - python_bin = "python.exe" if ("windows" in platform) else "bin/python3" + uid = int(stdout.strip()) + if uid == 0: + fail("The current user is root, please run as non-root when using the hermetic Python interpreter. See https://github.com/bazelbuild/rules_python/pull/713.") + else: + fail("The current user has CAP_DAC_OVERRIDE set, please drop this capability when using the hermetic Python interpreter. See https://github.com/bazelbuild/rules_python/pull/713.") glob_include = [] glob_exclude = [ @@ -529,6 +531,15 @@ For more information see the official bazel docs "zstd_version": attr.string( default = "1.5.2", ), + "_chmod": attr.label( + default = "//python/private:chmod.py", + ), + "_touch": attr.label( + default = "//python/private:touch.py", + ), + "_id": attr.label( + default = "//python/private:id.py", + ), }, environ = [REPO_DEBUG_ENV_VAR], )