giampaolo / psutil

Cross-platform lib for process and system monitoring in Python
BSD 3-Clause "New" or "Revised" License
10.31k stars 1.39k forks source link

[linux] how can psutil show `exe` if the linx is not readable? #2334

Open calestyo opened 11 months ago

calestyo commented 11 months ago

Summary

Description

I wondered how psutil the following can happen.

As a normal user:

$ id -u
1000
$ 
$ readlink /proc/3763/exe; echo $?
1
$ readlink /proc/4372/exe; echo $?
1

the exe information (and others) for these processes (which are Xorg and lightdm) cannot be read.
But their cmdline can:

$ hd /proc/3763/cmdline
00000000  2f 75 73 72 2f 6c 69 62  2f 78 6f 72 67 2f 58 6f  |/usr/lib/xorg/Xo|
00000010  72 67 00 3a 30 00 2d 73  65 61 74 00 73 65 61 74  |rg.:0.-seat.seat|
00000020  30 00 2d 61 75 74 68 00  2f 76 61 72 2f 72 75 6e  |0.-auth./var/run|
00000030  2f 6c 69 67 68 74 64 6d  2f 72 6f 6f 74 2f 3a 30  |/lightdm/root/:0|
00000040  00 2d 6e 6f 6c 69 73 74  65 6e 00 74 63 70 00 76  |.-nolisten.tcp.v|
00000050  74 37 00 2d 6e 6f 76 74  73 77 69 74 63 68 00     |t7.-novtswitch.|
0000005f
$ hd /proc/4372/cmdline
00000000  6c 69 67 68 74 64 6d 00  2d 2d 73 65 73 73 69 6f  |lightdm.--sessio|
00000010  6e 2d 63 68 69 6c 64 00  31 33 00 32 34 00        |n-child.13.24.|
0000001e

Now, when again as the same user (UID 1000), I do the following with psutil:

>>> import psutil
>>> p1=psutil.Process(pid=3763)
>>> p2=psutil.Process(pid=4372)
>>> p1.exe()
'/usr/lib/xorg/Xorg'

>>> p2.exe()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3/dist-packages/psutil/__init__.py", line 671, in exe
    return guess_it(fallback=err)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/psutil/__init__.py", line 664, in guess_it
    raise fallback
  File "/usr/lib/python3/dist-packages/psutil/__init__.py", line 669, in exe
    exe = self._proc.exe()
          ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/psutil/_pslinux.py", line 1767, in exe
    raise AccessDenied(self.pid, self._name)
psutil.AccessDenied: (pid=4372)

>>>

So if exe is unreadable in both cases, how can it know the exe in the first case?

My assumption is, that it somehow takes cmdline, but perhaps only if that starts with an absolute path?
I tried to look it up in the code, but couldn't find anything at a first glance: https://github.com/giampaolo/psutil/blob/master/psutil/_pslinux.py#L1763-L1773

If it would somehow use cmdline, that would IMO be a security hole. Processes can modify their own cmdline so when another process would e.g. trust that exe() returns the right executable, this might be wrong and so the user of exe() could be lead into doing bad things.

Any idea how your code does the magic to get the executable despite the symlink in /proc not being readable?

Thanks, Chris.

giampaolo commented 11 months ago

Magic explained: https://github.com/giampaolo/psutil/blob/4407540e720c46641622a33bb579430e047cd511/psutil/__init__.py#L662-L687. :)

calestyo commented 11 months ago

Well but as I wrote quite lengthy, this is actually a security hole as any process may thereby fake it's executable to be something else.

So could you please reopen and ... well simply not do this?

giampaolo commented 11 months ago

Sorry, I was in a rush and only focused on the first part of your post. I see your point. Reopening, but I'll have to think about this. Maybe we can add a guess=True argument.

calestyo commented 11 months ago

No worries.

Uhm, I would rather suggest to call it unsafe. That makes it much more of a heads up and people have a better chance to actually realise what's going on and make a proper choice.

And adding some warning box in the documentation, that when this is True processes might hide their own true executable, would be nice.