Skip to content

Commit 52bc75c

Browse files
authored
Merge branch 'master' into cve_fixes
2 parents 4148d2d + c32c086 commit 52bc75c

12 files changed

+87
-35
lines changed

gprofiler/main.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import configargparse
3333
import humanfriendly
34-
from granulate_utils.linux.ns import is_running_in_init_pid
34+
from granulate_utils.linux.ns import is_root, is_running_in_init_pid
3535
from granulate_utils.linux.process import is_process_running
3636
from granulate_utils.metadata.cloud import get_aws_execution_env
3737
from psutil import NoSuchProcess, Process
@@ -70,12 +70,11 @@
7070
atomically_symlink,
7171
get_iso8601_format_time,
7272
grab_gprofiler_mutex,
73-
is_root,
7473
reset_umask,
7574
resource_path,
7675
run_process,
7776
)
78-
from gprofiler.utils.fs import escape_filename, mkdir_owned_root
77+
from gprofiler.utils.fs import escape_filename, mkdir_owned_root_wrapper
7978
from gprofiler.utils.proxy import get_https_proxy
8079

8180
if is_linux():
@@ -121,6 +120,7 @@ def __init__(
121120
output_dir: str,
122121
flamegraph: bool,
123122
rotating_output: bool,
123+
rootless: bool,
124124
profiler_api_client: Optional[ProfilerAPIClient],
125125
collect_metrics: bool,
126126
collect_metadata: bool,
@@ -141,6 +141,7 @@ def __init__(
141141
self._output_dir = output_dir
142142
self._flamegraph = flamegraph
143143
self._rotating_output = rotating_output
144+
self._rootless = rootless
144145
self._profiler_api_client = profiler_api_client
145146
self._state = state
146147
self._remote_logs_handler = remote_logs_handler
@@ -566,6 +567,15 @@ def parse_cmd_args() -> configargparse.Namespace:
566567
help="Comma separated list of processes that will be filtered to profile,"
567568
" given multiple times will append pids to one list",
568569
)
570+
parser.add_argument(
571+
"--rootless",
572+
action="store_true",
573+
default=False,
574+
help="Run without root/sudo with limited functionality"
575+
"Profiling is limted to only processes owned by this user that are passed with --pids. Logs and pid file "
576+
"may be directed to user owned directory with --log-file and --pid-file respectively. Some additional "
577+
"configuration (e.g. kernel.perf_event_paranoid) may be required to operate without root.",
578+
)
569579

570580
_add_profilers_arguments(parser)
571581

@@ -903,8 +913,14 @@ def _add_profilers_arguments(parser: configargparse.ArgumentParser) -> None:
903913

904914

905915
def verify_preconditions(args: configargparse.Namespace, processes_to_profile: Optional[List[Process]]) -> None:
906-
if not is_root():
907-
print("Must run gprofiler as root, please re-run.", file=sys.stderr)
916+
if not args.rootless and not is_root():
917+
print("Not running as root, rerun with --rootless or as root.", file=sys.stderr)
918+
sys.exit(1)
919+
elif args.rootless and is_root():
920+
print(
921+
"Conflict, running with --rootless and as root, rerun with --rootless or as root (but not both).",
922+
file=sys.stderr,
923+
)
908924
sys.exit(1)
909925

910926
if args.pid_ns_check and not is_running_in_init_pid():
@@ -1102,7 +1118,7 @@ def main() -> None:
11021118
)
11031119
sys.exit(1)
11041120

1105-
mkdir_owned_root(TEMPORARY_STORAGE_PATH)
1121+
mkdir_owned_root_wrapper(TEMPORARY_STORAGE_PATH)
11061122

11071123
try:
11081124
client_kwargs = {}
@@ -1154,6 +1170,7 @@ def main() -> None:
11541170
output_dir=args.output_dir,
11551171
flamegraph=args.flamegraph,
11561172
rotating_output=args.rotating_output,
1173+
rootless=args.rootless,
11571174
profiler_api_client=profiler_api_client,
11581175
collect_metrics=args.collect_metrics,
11591176
collect_metadata=args.collect_metadata,

gprofiler/metadata/metadata_collector.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33
from typing import Optional
44

5-
from granulate_utils.linux.ns import run_in_ns
5+
from granulate_utils.linux.ns import run_in_ns_wrapper
66
from granulate_utils.metadata import Metadata
77
from granulate_utils.metadata.bigdata import get_bigdata_info
88
from granulate_utils.metadata.cloud import get_static_cloud_metadata
@@ -20,7 +20,7 @@ def get_static_metadata(spawn_time: float, run_args: UserArgs, external_metadata
2020
formatted_spawn_time = datetime.datetime.utcfromtimestamp(spawn_time).replace(microsecond=0).isoformat()
2121
static_system_metadata = get_static_system_info()
2222
cloud_metadata = get_static_cloud_metadata(logger)
23-
bigdata = run_in_ns(["mnt"], get_bigdata_info)
23+
bigdata = run_in_ns_wrapper(["mnt"], get_bigdata_info)
2424

2525
metadata_dict: Metadata = {
2626
"cloud_provider": cloud_metadata.pop("provider") if cloud_metadata is not None else "unknown",

gprofiler/metadata/system_metadata.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import distro
1717
import psutil
18-
from granulate_utils.linux.ns import run_in_ns
18+
from granulate_utils.linux.ns import run_in_ns_wrapper
1919

2020
from gprofiler.log import get_logger_adapter
2121
from gprofiler.platform import is_linux, is_windows
@@ -357,7 +357,7 @@ def get_infos() -> Any:
357357
except Exception:
358358
logger.exception("Failed to get the local IP")
359359

360-
run_in_ns(["mnt", "uts", "net"], get_infos)
360+
run_in_ns_wrapper(["mnt", "uts", "net"], get_infos)
361361

362362
return hostname, distribution, libc_version, mac_address, local_ip
363363

gprofiler/metadata/versions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from subprocess import CompletedProcess
1717
from threading import Event
1818

19-
from granulate_utils.linux.ns import get_process_nspid, run_in_ns
19+
from granulate_utils.linux.ns import get_process_nspid, run_in_ns_wrapper
2020
from psutil import NoSuchProcess, Process
2121

2222
from gprofiler.utils import run_process
@@ -38,7 +38,7 @@ def _run_get_version() -> "CompletedProcess[bytes]":
3838
return run_process([exe_path, version_arg], stop_event=stop_event, timeout=get_version_timeout)
3939

4040
try:
41-
cp = run_in_ns(["pid", "mnt"], _run_get_version, process.pid)
41+
cp = run_in_ns_wrapper(["pid", "mnt"], _run_get_version, process.pid)
4242
except FileNotFoundError as e:
4343
if not process.is_running():
4444
raise NoSuchProcess(process.pid)

gprofiler/profilers/java.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@
4949
if is_linux():
5050
from granulate_utils.linux import proc_events
5151
from granulate_utils.linux.kernel_messages import KernelMessage
52-
from granulate_utils.linux.ns import get_proc_root_path, get_process_nspid, resolve_proc_root_links, run_in_ns
52+
from granulate_utils.linux.ns import (
53+
get_proc_root_path,
54+
get_process_nspid,
55+
resolve_proc_root_links,
56+
run_in_ns_wrapper,
57+
)
5358
from granulate_utils.linux.oom import get_oom_entry
5459
from granulate_utils.linux.process import (
5560
get_mapped_dso_elf_id,
@@ -354,7 +359,7 @@ def _run_java_version() -> "CompletedProcess[bytes]":
354359

355360
# doesn't work without changing PID NS as well (I'm getting ENOENT for libjli.so)
356361
# Version is printed to stderr
357-
return run_in_ns(["pid", "mnt"], _run_java_version, process.pid).stderr.decode().strip()
362+
return run_in_ns_wrapper(["pid", "mnt"], _run_java_version, process.pid).stderr.decode().strip()
358363

359364

360365
def get_java_version_logged(process: Process, stop_event: Event) -> Optional[str]:

gprofiler/profilers/node.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import psutil
2929
import requests
30-
from granulate_utils.linux.ns import get_proc_root_path, get_process_nspid, resolve_proc_root_links, run_in_ns
30+
from granulate_utils.linux.ns import get_proc_root_path, get_process_nspid, resolve_proc_root_links, run_in_ns_wrapper
3131
from granulate_utils.linux.process import is_musl, is_process_running
3232
from retry import retry
3333
from websocket import create_connection
@@ -246,7 +246,7 @@ def generate_map_for_node_processes(processes: List[psutil.Process]) -> List[psu
246246
nspid = get_process_nspid(process.pid)
247247
ns_link_name = os.readlink(f"/proc/{process.pid}/ns/pid")
248248
_start_debugger(process.pid)
249-
run_in_ns(
249+
run_in_ns_wrapper(
250250
["pid", "mnt", "net"],
251251
lambda: _generate_perf_map(dest, nspid, ns_link_name, process.pid),
252252
process.pid,
@@ -268,7 +268,7 @@ def clean_up_node_maps(processes: List[psutil.Process]) -> None:
268268
ns_link_name = os.readlink(f"/proc/{process.pid}/ns/pid")
269269
dest = _get_dest_inside_container(is_musl(process), node_major_version)
270270
_start_debugger(process.pid)
271-
run_in_ns(
271+
run_in_ns_wrapper(
272272
["pid", "mnt", "net"],
273273
lambda: _clean_up(dest, nspid, ns_link_name, process.pid),
274274
process.pid,

gprofiler/profilers/perf.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@ def __init__(
194194
try:
195195
# We want to be certain that `perf record` will collect samples.
196196
discovered_perf_event = discover_appropriate_perf_event(
197-
Path(self._profiler_state.storage_dir), self._profiler_state.stop_event
197+
Path(self._profiler_state.storage_dir),
198+
self._profiler_state.stop_event,
199+
self._profiler_state.processes_to_profile,
198200
)
199201
logger.debug("Discovered perf event", discovered_perf_event=discovered_perf_event.name)
200202
extra_args.extend(discovered_perf_event.perf_extra_args())

gprofiler/profilers/python.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from typing import Any, Dict, List, Match, Optional, cast
2323

2424
from granulate_utils.linux.elf import get_elf_id
25-
from granulate_utils.linux.ns import get_process_nspid, run_in_ns
25+
from granulate_utils.linux.ns import get_process_nspid, run_in_ns_wrapper
2626
from granulate_utils.linux.process import (
2727
get_mapped_dso_elf_id,
2828
is_process_basename_matching,
@@ -140,7 +140,7 @@ def _run_python_process_in_ns() -> "CompletedProcess[bytes]":
140140
timeout=self._PYTHON_TIMEOUT,
141141
)
142142

143-
return run_in_ns(["pid", "mnt"], _run_python_process_in_ns, process.pid).stdout.decode().strip()
143+
return run_in_ns_wrapper(["pid", "mnt"], _run_python_process_in_ns, process.pid).stdout.decode().strip()
144144
except Exception:
145145
return None
146146

gprofiler/utils/__init__.py

+7-11
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
import psutil
4040
from granulate_utils.exceptions import CouldNotAcquireMutex
4141
from granulate_utils.linux.mutex import try_acquire_mutex
42-
from granulate_utils.linux.ns import run_in_ns
42+
from granulate_utils.linux.ns import is_root, run_in_ns_wrapper
4343
from granulate_utils.linux.process import is_kernel_thread, process_exe
4444
from psutil import Process
4545

@@ -82,14 +82,6 @@ def resource_path(relative_path: str = "") -> str:
8282
raise Exception(f"Resource {relative_path!r} not found!") from e
8383

8484

85-
@lru_cache(maxsize=None)
86-
def is_root() -> bool:
87-
if is_windows():
88-
return cast(int, ctypes.windll.shell32.IsUserAnAdmin()) == 1 # type: ignore
89-
else:
90-
return os.geteuid() == 0
91-
92-
9385
libc: Optional[ctypes.CDLL] = None
9486

9587

@@ -376,7 +368,11 @@ def pgrep_maps(match: str) -> List[Process]:
376368
for line in result.stderr.splitlines():
377369
if not (
378370
line.startswith(b"grep: /proc/")
379-
and (line.endswith(b"/maps: No such file or directory") or line.endswith(b"/maps: No such process"))
371+
and (
372+
line.endswith(b"/maps: No such file or directory")
373+
or line.endswith(b"/maps: No such process")
374+
or (not is_root() and b"/maps: Permission denied" in line)
375+
)
380376
):
381377
error_lines.append(line)
382378
if error_lines:
@@ -455,7 +451,7 @@ def grab_gprofiler_mutex() -> bool:
455451
GPROFILER_LOCK = "\x00gprofiler_lock"
456452

457453
try:
458-
run_in_ns(["net"], lambda: try_acquire_mutex(GPROFILER_LOCK))
454+
run_in_ns_wrapper(["net"], lambda: try_acquire_mutex(GPROFILER_LOCK))
459455
except CouldNotAcquireMutex:
460456
print(
461457
"Could not acquire gProfiler's lock. Is it already running?"

gprofiler/utils/fs.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
from secrets import token_hex
2222
from typing import Union
2323

24+
from granulate_utils.linux.ns import is_root
25+
2426
from gprofiler.platform import is_windows
25-
from gprofiler.utils import is_root, remove_path, run_process
27+
from gprofiler.utils import remove_path, run_process
2628

2729

2830
def safe_copy(src: str, dst: str) -> None:
@@ -76,6 +78,32 @@ def is_owned_by_root(path: Path) -> bool:
7678
return statbuf.st_uid == 0 and statbuf.st_gid == 0
7779

7880

81+
def mkdir_owned_root_wrapper(path: Union[str, Path], mode: int = 0o755) -> None:
82+
"""
83+
Ensures a directory exists and writable.
84+
85+
If the directory exists and is not writable, the function raises.
86+
If the directory exists and is not writable, it is left as is.
87+
If the directory doesn't exist, it is created.
88+
"""
89+
if is_root():
90+
return mkdir_owned_root(path)
91+
92+
path = path if isinstance(path, Path) else Path(path)
93+
if path.exists():
94+
if not os.access(path, os.W_OK):
95+
raise Exception(f"{str(path)} is not writable by current user")
96+
return
97+
98+
try:
99+
os.mkdir(path, mode=mode)
100+
except FileExistsError:
101+
# likely racing with another thread of gprofiler. as long as the directory is the user after all, we're good.
102+
if not os.access(path, os.W_OK):
103+
raise Exception(f"{str(path)} is not writable by current user")
104+
pass
105+
106+
79107
def mkdir_owned_root(path: Union[str, Path], mode: int = 0o755) -> None:
80108
"""
81109
Ensures a directory exists and is owned by root.

gprofiler/utils/perf.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from threading import Event
2222
from typing import List, Optional
2323

24+
from psutil import Process
25+
2426
from gprofiler.exceptions import CalledProcessError, PerfNoSupportedEvent
2527
from gprofiler.gprofiler_types import ProcessToStackSampleCounters
2628
from gprofiler.log import get_logger_adapter
@@ -66,7 +68,9 @@ def perf_extra_args(self) -> List[str]:
6668
return ["-e", self.value]
6769

6870

69-
def discover_appropriate_perf_event(tmp_dir: Path, stop_event: Event) -> SupportedPerfEvent:
71+
def discover_appropriate_perf_event(
72+
tmp_dir: Path, stop_event: Event, pids: Optional[List[Process]] = None
73+
) -> SupportedPerfEvent:
7074
"""
7175
Get the appropriate event should be used by `perf record`.
7276
@@ -95,7 +99,7 @@ def discover_appropriate_perf_event(tmp_dir: Path, stop_event: Event) -> Support
9599
is_dwarf=False,
96100
inject_jit=False,
97101
extra_args=current_extra_args,
98-
processes_to_profile=None,
102+
processes_to_profile=pids,
99103
switch_timeout_s=15,
100104
)
101105
perf_process.start()

tests/test_preconditions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def test_not_root(
102102
wait_for_container(gprofiler)
103103

104104
assert e.value.exit_status == 1
105-
assert e.value.stderr == b"Must run gprofiler as root, please re-run.\n"
105+
assert e.value.stderr == b"Not running as root, rerun with --rootless or as root.\n"
106106

107107

108108
def test_not_host_pid(

0 commit comments

Comments
 (0)