Skip to content

Commit 6360449

Browse files
authored
Ensure directories used by gProfiler to host async-profiler DSO are owned by root (#832)
1 parent 908d5d3 commit 6360449

File tree

3 files changed

+81
-18
lines changed

3 files changed

+81
-18
lines changed

gprofiler/main.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
resource_path,
6666
run_process,
6767
)
68-
from gprofiler.utils.fs import escape_filename
68+
from gprofiler.utils.fs import escape_filename, mkdir_owned_root
6969
from gprofiler.utils.proxy import get_https_proxy
7070

7171
if is_linux():
@@ -954,6 +954,7 @@ def main() -> None:
954954
if is_windows() or get_aws_execution_env() == "AWS_ECS_FARGATE":
955955
args.perf_mode = "disabled"
956956
args.pid_ns_check = False
957+
957958
if args.subcommand != "upload-file":
958959
verify_preconditions(args, processes_to_profile)
959960

@@ -1021,8 +1022,7 @@ def main() -> None:
10211022
)
10221023
sys.exit(1)
10231024

1024-
if not os.path.exists(TEMPORARY_STORAGE_PATH):
1025-
os.mkdir(TEMPORARY_STORAGE_PATH)
1025+
mkdir_owned_root(TEMPORARY_STORAGE_PATH)
10261026

10271027
try:
10281028
client_kwargs = {}

gprofiler/profilers/java.py

+33-10
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from subprocess import CompletedProcess
1414
from threading import Event, Lock
1515
from types import TracebackType
16-
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Type, TypeVar, Union
16+
from typing import Any, Dict, Iterable, List, Optional, Set, Type, TypeVar, Union
1717

1818
import psutil
1919
from granulate_utils.java import (
@@ -79,7 +79,7 @@
7979
touch_path,
8080
wait_event,
8181
)
82-
from gprofiler.utils.fs import is_rw_exec_dir, safe_copy
82+
from gprofiler.utils.fs import is_owned_by_root, is_rw_exec_dir, mkdir_owned_root, safe_copy
8383
from gprofiler.utils.perf import can_i_use_perf_events
8484
from gprofiler.utils.process import process_comm, search_proc_maps
8585

@@ -471,9 +471,10 @@ def __init__(
471471
# because storage_dir changes between runs.
472472
# we embed the async-profiler version in the path, so future gprofiler versions which use another version
473473
# of AP case use it (will be loaded as a different DSO)
474+
self._ap_dir_base = self._find_rw_exec_dir()
475+
self._ap_dir_versioned = os.path.join(self._ap_dir_base, f"async-profiler-{get_ap_version()}")
474476
self._ap_dir_host = os.path.join(
475-
self._find_rw_exec_dir(POSSIBLE_AP_DIRS),
476-
f"async-profiler-{get_ap_version()}",
477+
self._ap_dir_versioned,
477478
"musl" if self._needs_musl_ap() else "glibc",
478479
)
479480

@@ -498,20 +499,42 @@ def __init__(
498499
self._collect_meminfo = collect_meminfo
499500
self._include_method_modifiers = ",includemm" if include_method_modifiers else ""
500501

501-
def _find_rw_exec_dir(self, available_dirs: Sequence[str]) -> str:
502+
def _find_rw_exec_dir(self) -> str:
502503
"""
503504
Find a rw & executable directory (in the context of the process) where we can place libasyncProfiler.so
504505
and the target process will be able to load it.
506+
This function creates the gprofiler_tmp directory as a directory owned by root, if it doesn't exist under the
507+
chosen rwx directory.
508+
It does not create the parent directory itself, if it doesn't exist (e.g /run).
509+
The chosen rwx directory needs to be owned by root.
505510
"""
506-
for d in available_dirs:
507-
full_dir = resolve_proc_root_links(self._process_root, d)
511+
for d in POSSIBLE_AP_DIRS:
512+
full_dir = Path(resolve_proc_root_links(self._process_root, d))
513+
if not full_dir.parent.exists():
514+
continue # we do not create the parent.
515+
516+
if not is_owned_by_root(full_dir.parent):
517+
continue # the parent needs to be owned by root
518+
519+
mkdir_owned_root(full_dir)
520+
508521
if is_rw_exec_dir(full_dir):
509-
return full_dir
522+
return str(full_dir)
510523
else:
511-
raise NoRwExecDirectoryFoundError(f"Could not find a rw & exec directory out of {available_dirs}!")
524+
raise NoRwExecDirectoryFoundError(
525+
f"Could not find a rw & exec directory out of {POSSIBLE_AP_DIRS} for {self._process_root}!"
526+
)
512527

513528
def __enter__(self: T) -> T:
514-
os.makedirs(self._ap_dir_host, 0o755, exist_ok=True)
529+
# create the directory structure for executable libap, make sure it's owned by root
530+
# for sanity & simplicity, mkdir_owned_root() does not support creating parent directories, as this allows
531+
# the caller to absentmindedly ignore the check of the parents ownership.
532+
# hence we create the structure here part by part.
533+
assert is_owned_by_root(
534+
Path(self._ap_dir_base)
535+
), f"expected {self._ap_dir_base} to be owned by root at this point"
536+
mkdir_owned_root(self._ap_dir_versioned)
537+
mkdir_owned_root(self._ap_dir_host)
515538
os.makedirs(self._storage_dir_host, 0o755, exist_ok=True)
516539

517540
self._check_disk_requirements()

gprofiler/utils/fs.py

+45-5
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
import shutil
99
from pathlib import Path
1010
from secrets import token_hex
11+
from typing import Union
1112

1213
from gprofiler.platform import is_windows
13-
from gprofiler.utils import remove_path, run_process
14+
from gprofiler.utils import is_root, remove_path, run_process
1415

1516

1617
def safe_copy(src: str, dst: str) -> None:
@@ -23,18 +24,19 @@ def safe_copy(src: str, dst: str) -> None:
2324
os.rename(dst_tmp, dst)
2425

2526

26-
def is_rw_exec_dir(path: str) -> bool:
27+
def is_rw_exec_dir(path: Path) -> bool:
2728
"""
2829
Is 'path' rw and exec?
2930
"""
31+
assert is_owned_by_root(path), f"expected {path} to be owned by root!"
32+
3033
# randomize the name - this function runs concurrently on paths of in same mnt namespace.
31-
test_script = Path(path) / f"t-{token_hex(10)}.sh"
34+
test_script = path / f"t-{token_hex(10)}.sh"
3235

3336
# try creating & writing
3437
try:
35-
os.makedirs(path, 0o755, exist_ok=True)
3638
test_script.write_text("#!/bin/sh\nexit 0")
37-
test_script.chmod(0o755)
39+
test_script.chmod(0o755) # make sure it's executable. file is already writable only by root due to umask.
3840
except OSError as e:
3941
if e.errno == errno.EROFS:
4042
# ro
@@ -56,3 +58,41 @@ def is_rw_exec_dir(path: str) -> bool:
5658

5759
def escape_filename(filename: str) -> str:
5860
return filename.replace(":", "-" if is_windows() else ":")
61+
62+
63+
def is_owned_by_root(path: Path) -> bool:
64+
statbuf = path.stat()
65+
return statbuf.st_uid == 0 and statbuf.st_gid == 0
66+
67+
68+
def mkdir_owned_root(path: Union[str, Path], mode: int = 0o755) -> None:
69+
"""
70+
Ensures a directory exists and is owned by root.
71+
72+
If the directory exists and is owned by root, it is left as is.
73+
If the directory exists and is not owned by root, it is removed and recreated. If after recreation
74+
it is still not owned by root, the function raises.
75+
"""
76+
assert is_root() # this function behaves as we expect only when run as root
77+
78+
path = path if isinstance(path, Path) else Path(path)
79+
# parent is expected to be root - otherwise, after we create the root-owned directory, it can be removed
80+
# as re-created as non-root by a regular user.
81+
if not is_owned_by_root(path.parent):
82+
raise Exception(f"expected {path.parent} to be owned by root!")
83+
84+
if path.exists():
85+
if is_owned_by_root(path):
86+
return
87+
88+
shutil.rmtree(path)
89+
90+
try:
91+
os.mkdir(path, mode=mode)
92+
except FileExistsError:
93+
# likely racing with another thread of gprofiler. as long as the directory is root after all, we're good.
94+
pass
95+
96+
if not is_owned_by_root(path):
97+
# lost race with someone else?
98+
raise Exception(f"Failed to create directory {str(path)} as owned by root")

0 commit comments

Comments
 (0)