Some notes about questions that came to my mind while reading Julia Evans’ zine Bite Size Linux.
What are the setuid, setgid and sticky bits of a file?
What’s inside /proc/PID/stack?
Why would I want to use mkfifo? (named pipes)
What are Unix domain sockets and why would I want to use them?
Can I see a process’ registered signal handlers from /proc/PID?
What is a memory allocator? Can I write one by myself?
What is ldd?
What is the difference between the fork and the clone system calls?
What is mmap? What happens if two different processes mmap the same file?
What is man 6 sl
?
What are the setuid, setgid and sticky bits of a file?
setuid: if the file is an executable, allow users with execution permission to run it with the privileges of the file’s owner. If the file is a directory, no-op.
setgid: if the file is an executable, allow users with execution permission to run it with the privileges of the file’s group. If the target is a directory, new files and subdirectories in it will inherit its group ID, rather than the primary group ID of the user who’ll create the file.
sticky: if the file is an executable, no-op. If the file is a directory, only the owner of files/sub-directories can delete/rename them. Normally this would be possible also for other users that belong to the group of the file/subdirectory. Beware that other users can still edit files’ content if they have enough privileges.
Why would you want to use them?
setuid (or setuid) on executables: to allow users with limited privileges to run
trusted programs that require higher ones. E.g. the passwd
command needs root
privileges to edit the /etc/shadow
and /etc/passwd
files, but you can run it as a
normal user thanks to its setuid
flag.
setgid on directories: to avoid messing with permissions of files and subdirectories in a shared workspace - by default new ones created by a user wouldn’t be accessible to others.
sticky on directories: to avoid users deleting or renaming other users’ files or
directories. It is used for example in Linux on the /tmp
directory.
You can check a file’s permissions bits with stat -c %a
on Linux or stat -f %p
on BSDs.
More resources:
What’s inside /proc/PID/stack?
There’s the stack trace in case your program is executing a system call!
Watch out that you’ll see it as an empty file unless you have the CAP_SYS_PTRACE
capability (check man 5 proc
and man 2 ptrace
for more info).
Try out the following:
- Launch
sleep 600
in a shell. - Get its PID with
ps au | grep 'sleep 600' | grep -v grep | awk '{print $2}'
. - Do
cat /proc/PID/stack
(you should get an empty output). - Get root with
sudo su
. - Now try again
cat /proc/$PID/stack
:)
Why would I want to use mkfifo? (named pipes)
They can be super useful if you want to plumb together two programs that don’t read from stdin or write to stdout but take files as arguments!
Stupid example
You have a program genturtle
that takes a filename in input and
writes an ASCII art turtle into it.
#!/usr/bin/env python
import sys
TURTLE = r""" ___-------___
_-~~ ~~-_
_-~ /~-_
/^\__/^\ /~ \ / \
/| O|| O| / \_______________/ \
| |___||__| / / \ \
| \ / / \ \
| (_______) /______/ \_________ \
| / / \ / \
\ \^\\ \ / \ /
\ || \______________/ _-_ //\__//
\ ||------_-~~-_ ------------- \ --/~ ~\ || __/
~-----||====/~ |==================| |/~~~~~
(_(__/ ./ / \_\ \.
(_(___/ -jurcy- \_____)_)"""
def main():
with open(sys.argv[1], "w") as fp:
fp.write(TURTLE)
if __name__ == "__main__":
main()
You have another program marginate
that takes the name of a text file in input, reads
it and prints its contents, prepending some characters to each line.
#!/usr/bin/env python
import sys
def main():
with open(sys.argv[1], "r") as fp:
lines = fp.readlines()
print("".join([f"@@@{line}" for line in lines]))
if __name__ == "__main__":
main()
You would like to run these programs to print a marginated turtle on standard
output but unfortunately neither genturtle
prints to stdout nor marginate
reads from stdin.
mkfifo
comes to the rescue!
- Create a named pipe with mkfifo:
mkfifo turtle
. - Use it as argument for both
genturtle
andmarginate
:(./genturtle turtle & ./marginate turtle)
Pay attention to how you run genturtle
and marginate
- depending on their
execution order your shell could get stuck forever (e.g. ./genturtle turtle && ./marginate turtle
). The reason is that genturtle
’s attempt to write to turtle
will block until marginate
will read from it.
In this case, it would have been straightforward to edit the source code of genturtle
and marginate
to print to stdout and read from stdin, or you could have just used a
temporary file. But think how handy mkfifo
would have been if they both were compiled
programs whose code you didn’t have access to and if they had to process an
enormous quantity of data you couldn’t store on your disk.
What are Unix domain sockets and why would I want to use them?
Check man 7 unix
.
They are special sockets for efficient interprocess communication.
Some key features:
- UDP unix domain sockets are reliable.
- You can pass references to file descriptors with them.
They are used for example by docker! :)
Can I see a process’ registered signal handlers from /proc/PID?
You can see which signals a process will block, ignore or catch by looking at SigBlk
,
SigIgn
, SigCgt
in /proc/PID/status
. These values are bitmasks expressed in
hexadecimal.
For example, SigIgn: 000000000a200000
means:
000000000a200000 -> 1010001000000000000000000000 (count from right to left)
| | | |
| | | \ --> 1: SIGHUP
| | \ -----------------------> 22: SIGTTOU
| \ ---------------------------> 26: SIGVTALRM
\ -----------------------------> 28: SIGWINCH
Our process will ignore
these four signals!
Unfortunately, you can’t see what handlers are registered for each specific
signal. Well, at least not unless you can inject some code into the process. In
this case, you could use the sigaction
system call and write this information
somewhere.
More resources:
man 5 proc
man 7 signal
- How can I check what signals a process is listening to?
- How to check the signal handler in Linux?
What is a memory allocator? Can I write one by myself?
Usually, by memory allocator one refers to a library that takes care to manage
a program’s heap. Processes can get more heap either by using the sbrk
system call or by creating an anonymous memory map with mmap
. A memory
allocator builds on these system calls to provide a handy abstraction for the
programmer.
Of course you can! You could even inject it into existing programs if they make
use of dynamic linking and you implement the API of malloc
. Take a look at how
malloc is implemented in musl libc or jemalloc for example.
More resources:
What is ldd?
ldd
allows you to see what shared libraries a dynamically linked program depends on
and where they are located on your system.
Where the dynamic linker looks:
- DT_RPATH in executable.
- LD_LIBRARY_PATH environment variable.
- DT_RUNPATH in executable.
- /etc/ld.so.cache
- /lib and /usr/lib
This means you can sometimes inject custom libraries into executables with LD_LIBRARY_PATH! :) (attack vector anyone?)
You may think that setuid
/setgid
and dynamic linking could be a fatal combo, and
indeed they would be, were it not that Linux ignores LD_LIBRARY_PATH in this case for
this very reason.
More resources:
man 1 ldd
man 8 ld.so
man 8 ldconfig
- Exploiting LD_LIBRARY_PATH
What is the difference between the fork and the clone system calls?
clone
is a lower level system call that provides more granular control over what pieces
of execution context are shared between the calling process and the child process.
More resources:
man 2 fork
man 2 clone
What is mmap? What happens if two different processes mmap the same file?
mmap
is a system call that allows you to map files to your program’s virtual
memory or to allocate new memory for your process (anonymous maps).
You can use it to process files that would be too big to fit in your RAM, to get
more memory for your program (see how allocators like jemalloc
use it), or to
share memory between different processes.
Linux is smart! If two different processes mmap the same file, the kernel will (eventually) load it into RAM only one time :)
Try out the following:
Create a huge file:
head -c 16000000000 /dev/random | pv > bigfile.bin
Create a program
load
that mmaps a file it takes in input, iterates over its content (to cause page faults and force the kernel to load it in memory), then waits for input before exiting:#!/usr/bin/env python import mmap import sys def main(): with open(sys.argv[1], "r") as fp: mm = mmap.mmap(fp.fileno(), 0, prot=mmap.PROT_READ) for i in range(len(mm) // 2): mm[i] input("Press enter to close the program") if __name__ == "__main__": main()
Launch it to process the file you created in step 1:
python3 launch bigfile.bin
In another shell, launch the same program again.
Now in a third shell, check their memory usage with top or any other similar tool.
More resources:
man 2 mmap
man 3 mmap
man 2 mmap2
What is man 6 sl
?
On Arch its man page is located in section 1.