Skip to content

Commit

Permalink
fix: make Python repositories chmod/id/touch hermetic
Browse files Browse the repository at this point in the history
  • Loading branch information
mattyclarkson committed Jul 2, 2024
1 parent 084b877 commit e9b91f5
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 27 deletions.
77 changes: 77 additions & 0 deletions python/private/chmod.py
Original file line number Diff line number Diff line change
@@ -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()
45 changes: 45 additions & 0 deletions python/private/id.py
Original file line number Diff line number Diff line change
@@ -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()
43 changes: 43 additions & 0 deletions python/private/touch.py
Original file line number Diff line number Diff line change
@@ -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()
65 changes: 38 additions & 27 deletions python/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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],
)
Expand Down

0 comments on commit e9b91f5

Please sign in to comment.