Linux: Force-Closing Leaked Sockets

This article describes a way of closing network sockets that were left open as a result of a programming error in the code of a library that cannot be (promptly) fixed.

The following circumstances may sound very familiar to the reader: I used an open-source networking library that suited me very well except for the handling of sockets: the library assumed that the main process will never outlive the TCP server that the library implemented and there is no need to close the listening socket. Unfortunately, I had to have the option of restarting the server while keeping the process, so this behaviour of the library was very irritating.

Contacting the author of the package seemed to take more time than I had. Fixing the bug myself was not an option: my users insisted on installing mainstream versions of the library (No patches, please. We do verify signatures.)

On Linux it is possible, with a certain effort, to map an IP address and a TCP port number to an open file descriptor with the help of the proc(5) pseudo-filesystem. The following code is an ad-hoc version for matching local TCP/IPv4 addresses and ports but the idea can be generalised for UDP, UNIX sockets, and pipes. It also possible to map remote addresses or combinations of local addresses and remote addresses.

First of all, I needed three Common Lisp packages (all of them can be installed with Quicklisp):

 1    (require :cffi)
 2    (require :usocket)
 3    (require :osicat)

Unfortunately, osicat:read-link does more than a simple call to readlink(2), and it fails when a symbolic link points to something which is not a file name. I had to roll my own version:

 5    (defun readlink (path)
 6      (cffi:with-foreign-object (buf :char 4096)
 7        (let* ((len (cffi:foreign-funcall "readlink"
 8                                          :string path
 9                                          :string buf
10                                          :uint 4096
11                                          :int)))
12          (when (plusp len)
13            (let ((name (make-string len)))
14              (dotimes (i len)
15                (setf (elt name i) (code-char (cffi:mem-ref buf :char i))))
16              name)))))
This version allocates much more memory than it is required for the task and the proper way would be calling lstat(2) first.

Then I needed to build a map of paths to file descriptors, as described by the contents of /proc/PID/fd:

18    (defun paths-to-fds ()
19      (let* ((table (make-hash-table :test 'equal))
20             (dirname (format nil "/proc/~D/fd"
21                              (cffi:foreign-funcall "getpid" :int)))
22             (listing (osicat:list-directory dirname :bare-pathnames t)))
23        (mapcar #'(lambda (entry)
24                    (let* ((fd (parse-integer (format nil "~A" entry)
25                                              :junk-allowed t))
26                           (path (format nil "~A/~D" dirname fd))
27                           (name (readlink path)))
28                      (when name
29                        (setf (gethash name table) fd))))
30                listing)
31        table))

I also needed a helper function that would split a line of text on a whitespace and return the pieces in the form of a list of strings (I am sorry, it is not tail-recursive):

33    (defun split-record (string &optional (pos 0))
34      (let ((nws (position-if #'(lambda (char) (not (whitespacep char)))
35                              string
36                              :start pos)))
37        (when nws
38          (let ((ws (position-if #'whitespacep string :start nws)))
39            (cons (subseq string nws ws) (split-record string ws))))))

Armed with the helper, I was able to read the list of TCP/IPv4 sockets that are open by the current process from /proc/PID/net/tcp into a list of "records":

41    (defun tcp-sockets ()
42      (let ((path (format nil "/proc/~D/net/tcp"
43                          (cffi:foreign-funcall "getpid" :int))))
44        (with-open-file (tcp path)
45          (loop for line = (read-line tcp nil nil)
46             while line collect (split-record line)))))

On a little-endian machine, IP addresses appear in /proc/PID/net/tcp in the host byte order while port numbers appear in the network byte order. Here is a helper function that formats the dotted notation of an IP address and a port number as it would be shown in /proc/PID/net/tcp:

48    (defun format-name (ip port)
49      (format nil "~8,'0X:~4,'0X"
50              (usocket:ip-from-octet-buffer
51                (usocket:dotted-quad-to-vector-quad ip)))
52              port)

Now it is trivial to find the inode that corresponds to a TCP socket that is bound to a local IPv4 address (the inode is the 9th field of the socket record):

55    (defun inode-by-local-address (dotted-quad port)
56      (nth 9 (find (format-name dotted-quad port)
57                   (tcp-sockets)
58                   :test #'string=
59                   :key #'second)))

Finally, finding the file discriptor becomes possible:

61    (defun fd-by-local-address (dotted-quad port)
62      (gethash (format nil
63                       "socket:[~D]"
64                       (inode-by-local-address dotted-quad port))
65               (paths-to-fds)))

Having the definitions given above, it is easy to find the file descriptor that corresponds to a listening socket that is bound to on the port number 1234:

      (fd-by-local-address "" 1234)	    
The socket can be closed using the following form:
      (let ((fd (fd-by-local-address "" 1234)))
        (when fd
          (cffi:foreign-funcall "close" :int fd :int)))

The complete source code is available here.

Vadim Penzin, February 21st, 2016

I hereby place this article along with the accompanying source code into the public domain.
You are welcome to contact me by writing to howto at this domain.
I publish this information in the hope that it will be useful, but without ANY WARRANTY.
You are responsible for any and all consequences that may arise as the result of using this information.