< up >
2023-07-19

linux file observability

In linux, “everything” is a file1. Regardless if its a regular file, directory, socket or even a device. Know how to observe open files, attached to a process, can become quite handy for debugging a linux host.

The first theoretical part will introduce file descriptors while the second part will give some practical examples.

Files and file descriptors

Whenever a program opens a file via a syscall from the open() family, the kernel returns a reference number that is unique for that process. This number is bettern known as file descriptor or fd for short. The program can act on that file (read, write, close,…) by using the fd from the open call.

Imagine a program like this:

// gcc example.c -o example
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int arc, char *argv[]){
    open("example.c", 0);

    while(1){sleep(1000);}
}

This program will open a file (itself for simplicity) and goes into an infinite loop in order to stay alive for observability.

On open the system creates a procfs entry under /proc/<pid>/fds/<fd> . Listing the proc dir for our process3 looks something like this:

$ ls -la /proc/1234/fd
total 0
dr-x------ 2 raphael raphael  0 Jun 29 21:11 .
dr-xr-xr-x 9 raphael raphael  0 Jun 29 21:11 ..
lrwx------ 1 raphael raphael 64 Jun 29 21:11 0 -> /dev/pts/0
lrwx------ 1 raphael raphael 64 Jun 29 21:11 1 -> /dev/pts/0
lrwx------ 1 raphael raphael 64 Jun 29 21:11 2 -> /dev/pts/0
lr-x------ 1 raphael raphael 64 Jun 29 21:11 3 -> /tmp/example.c

Observe

Wich process opened the file?

lsof got you covered:

$ lsof /tmp/example
COMMAND PID  USER  FD   TYPE DEVICE SIZE/OFF     NODE NAME
example 444 raphael txt    REG   0,40    23656 26759037 /tmp/example

Which files do a process holding?

It’s lsof again. The opened example.c can be seen at last with fd 3 linking to the original file. The other three fd’s 0 , 1 and 2 are the three pts devices for stdin, stdout stderr that get automatically created by the OS for any new process.

$ lsof -p 444
COMMAND PID    USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
example 444 raphael  cwd    DIR   8,32     4096 267422 /tmp/
example 444 raphael  rtd    DIR   8,32     4096      2 /
example 444 raphael  txt    REG   8,32    16088  46778 /tmp/example
example 444 raphael  mem    REG   8,32  2029592 142704 /usr/lib/...
example 444 raphael  mem    REG   8,32   191504 142576 /usr/lib/...
example 444 raphael    0u   CHR  136,0      0t0      3 /dev/pts/0
example 444 raphael    1u   CHR  136,0      0t0      3 /dev/pts/0
example 444 raphael    2u   CHR  136,0      0t0      3 /dev/pts/0
example 444 raphael    3r   REG   8,32      196  46772 /tmp/example.c

Is my server already listening for incoming connections?

Let’s spin up a tcp server waiting for incoming connections using ncat:

$ ncat -l 8080

Using lsof again, we can observe that ncat is even doing dual-stack waiting for incoming ipv4 and ipv6 connections, as one can see in the forelast entries.

$ lsof -p 532581
COMMAND    PID  USER   FD   TYPE             DEVICE SIZE/OFF     NODE NAME
ncat    532581 raphael  cwd    DIR               0,40      450 27369190 /tmp/example
ncat    532581 raphael  rtd    DIR               0,38      206      256 /
ncat    532581 raphael  txt    REG               0,38   435520  9123417 /usr/bin/ncat
ncat    532581 raphael  mem    REG               0,36           9123417 /usr/bin/ncat (path dev=0,38)
ncat    532581 raphael  mem    REG               0,36           9000771 /usr/lib64/...
ncat    532581 raphael    0u   CHR              136,1      0t0        4 /dev/pts/1
ncat    532581 raphael    1u   CHR              136,1      0t0        4 /dev/pts/1
ncat    532581 raphael    2u   CHR              136,1      0t0        4 /dev/pts/1
ncat    532581 raphael    3u  IPv6            9561146      0t0      TCP *:webcache (LISTEN)
ncat    532581 raphael    4u  IPv4            9561147      0t0      TCP *:webcache (LISTEN)
ncat    532581 raphael    6u  unix 0x0000000014cd383f      0t0    31426 type=STREAM (CONNECTED)

Let’s go one step further and establish a connection to ncat by browsing to localhost:8080. We can see that ncat closed the unused listener and from which address+port the connection is coming from:

$ lsof -p 532581
COMMAND    PID  USER   FD   TYPE             DEVICE SIZE/OFF     NODE NAME
ncat    532581 raphael  cwd    DIR               0,40      450 27369190 /tmp/example
ncat    532581 raphael  rtd    DIR               0,38      206      256 /
ncat    532581 raphael  txt    REG               0,38   435520  9123417 /usr/bin/ncat
ncat    532581 raphael  mem    REG               0,36           9123417 /usr/bin/ncat (path dev=0,38)
ncat    532581 raphael  mem    REG               0,36           9000771 /usr/lib64/...
ncat    532581 raphael    0u   CHR              136,1      0t0        4 /dev/pts/1
ncat    532581 raphael    1u   CHR              136,1      0t0        4 /dev/pts/1
ncat    532581 raphael    2u   CHR              136,1      0t0        4 /dev/pts/1
ncat    532581 raphael    5u  IPv4            9557641      0t0      TCP localhost:webcache->localhost:32886 (ESTABLISHED)
ncat    532581 raphael    6u  unix 0x0000000014cd383f      0t0    31426 type=STREAM (CONNECTED)

Does my process still hold fds to deleted files?

Let’s create a file and immediately unlink it but keep it open:

// gcc ulink.c -o ulink
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    open("test.log", O_CREAT);
    unlink("test.log");

    while(1){sleep(1000);}
    return 0;
}

The file is not existent, but can be observed via lsof:

$ lsof -p 635
COMMAND PID    USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
ulink   635 raphael  cwd    DIR   8,32     4096 267422 /tmp/example
ulink   635 raphael  rtd    DIR   8,32     4096      2 /
ulink   635 raphael  txt    REG   8,32    16168  50490 /tmp/example
ulink   635 raphael  mem    REG   8,32  2029592 142704 /usr/lib/...
ulink   635 raphael    0u   CHR  136,0      0t0      3 /dev/pts/0
ulink   635 raphael    1u   CHR  136,0      0t0      3 /dev/pts/0
ulink   635 raphael    2u   CHR  136,0      0t0      3 /dev/pts/0
ulink   635 raphael    3r   REG   8,32        0  40397 /tmp/test.log (deleted)

  1. Rabbit hole warning! source based on wikipedia
  2. Yes, I am looking at you PHP!
  3. use “prgrep -f example” to get the pid