Unix Domain Sockets: How to create a secure temp file name that doesn't exist as a file yet?

When using Unix Domain Sockets you need to pass tcp_bind(Socket, UnixDomainSocketFileName) a UnixDomainSocketFileName that is the name of a file that doesn’t exist yet. Turns out to be surprisingly complicated to do correctly (and securely).

Since using temp files improperly represents a security risk, I was hoping to use the tmp_file_stream/3 predicate to do this. My problem is that this predicate actually creates the file. My thought was to delete it immediately and just use the name and let the socket recreate it:

tmp_file_stream(UnixDomainSocketFileName, Stream, [extension(sock)]),
close(Stream),
delete_file(UnixDomainSocketFileName),
...
tcp_bind(Socket, UnixDomainSocketFileName)

I realize that this means the system won’t automatically clean it up anymore.

My concern is this phrase from the docs:

If the OS supports it, the created file is only accessible to the current user and the file is created using the open()-flag O_EXCL , which guarantees that the file did not exist before this call.

It appears that my approach will remove those two protections:

  • I’m OK losing the “guarantees that the file did not exist before this call” protection since the socket API fails if the file is already created. The system won’t be able to be tricked into using an existing file.
  • The problem is that I don’t have a way to have the file “only accessible to the current user” since the socket creates it and I’m pretty sure it doesn’t set the permissions that way (and that the tmp directory isn’t set that way).

The best approach I’ve come up with is to avoid using the above predicate altogether and create a special directory with permissions set to “this user only” and generate temp file names using a GUID so they aren’t guessable (or conflicting).

Does anyone know of a better approach?

I have some code, parts of which might help you. write_atomic_stream/3:
allows specifying which directory the tmp file is created in
adds a clean-up handler using at_halt/1, using “safe delete”

Thanks for sharing the code. So far, I’ve got the at_exit/1 working for the thread to do a safe delete already. My issue now is how to generate that filename (but not the file itself) in a safe way. The tcp predicates do the actual creating of the file.

Is this not what you seek?

unix_domain_socket/1

I ran into Unix Domain Sockets for the time during this topic: Docker API example

Yes, but then, as in that example, you need to pass a file to another predicate afterward to actually connect. That’s where I’m having my challenge.

1 Like

Not that I expect to be able to solve your problem but maybe if I understand it more I can help, or at least help by just asking questions that come to mind.

Does it have to be a file? Can it be just the file name, e.g. “/var/run/xyz.sock”, and then the other predicate uses the passed file name to open the socket.

EDIT

Found this which may help. (ref)

Security:

Unix sockets provide a mechanism for secure communication. Communication over unix sockets can be secured by controlling the file (or directory) permissions on the pathname sockets (or the parent directory). For example, the bind socket API creates a ‘socket’ file with the given pathname. The creation of the new socket file will fail if the calling process does not has write permission on the directory where the file is being created. Similarly, for connecting to a stream socket, the connecting process should have write permission on the socket. The same level of security is available and enforced on the Windows unix socket implementation. See the man page on AF_UNIX for more details on the security.

It must be a filename that doesn’t exist (which is why it is a bit tricky). I was honestly hoping there was just a predicate lying around that I could use for this Unix Domain Sockets case that was purpose built for it.

1 Like

tmp_file/2 creates a unique name. Its implementation is in swipl-devel/src/os/pl-os.c

And I have a little work-around, to put the temp file name in an arbitrary directory (set/restore prolog flag tmp_dir)

Most applications that use Unix domain sockets create them in a directory nobody else can write (create files) and often using a predefined name. From what I know you try to do you probably want multiple servers with their own unique socket. @peter.ludemann’s trick should work. I’d probably just create a random string and try to create the socket. If the bind fails with “Address already in use” you try again.

For security you should probably use a directory where only authorized connectors have access too. I guess the socket initial permissions are subject to the umask, but SWI-Prolog does not yet support umask handling. So, as is, the socket will be open after creation. When installed in a public directory attackers can connect before you change the permission (with some luck at least).

Yeah, I agree. Also, because the entire path needs to be < 92 bytes to be portable, it feels the only practical solution here is to have the predicate I’m building take the path to the file as an argument so that the caller can set up the system so that it meets all the requirements:

  • Path < 92 bytes
  • Folder permissions set for only authorized connectors
  • Filename doesn’t exist
  • Decide whether to reuse a name or create a new name each time

Thanks everyone!

This actually makes the system much less secure, because:

  1. transferring the responsibility of security to the caller (which presumably are other agents that want to connect to your system),
  2. because different callers may use different code to create the path (this increases the vector of attack)

I would suggest you create the path yourself using Peter’s model and Jan’s suggestion above, and set the umask when the server starts, by calling shell(‘umask <something>’).

I agree with you that, if we can just get the system to do the right thing automatically, that is better.

I need to see if @peter.ludemann’s suggestion going to work for all path lengths returned by tmp_file/2 since we have a 92 byte limit to be portable and it is unclear to me if the temp file directory is going to exceed that. I’m guessing the temp path directory is changeable by the user, etc.

Jan’s suggestion of generating a random file was actually what I started with, but then I realized that sockets generates that in the “current” directory, which could be anywhere, and thus has the same path length issue.

I could use @peter.ludemann’s workaround by default, but give an option to specify your own file if you need to guarantee it will work. But that just means you need to do the proposal we’re questioning anyway if you want to guarantee it will work.

I could potentially let the user specify a path+file that they know to be in the safe length and then do your umask suggestion to make it safer by locking it down. Of course that introduces a race where malicious code could modify the file during the race to set umask. I’m not sure how much this matters since I’m not sure that sockets is actually writing or reading anything to the file, so that might be OK. It does mean that malicious code could try to connect to the socket during the race since it will have permission to do that for a window.

The best two options I’ve come up with are:

  1. Discover that tmp_file/2 never generates files that are greater than 92 bytes and go with @peter.ludemann’s proposal
  2. Have the user specify a directory path that is < (say) 70 bytes long. Have my code set umask on it, and then create an unpredictably randomly named file within it that is (say) 22 bytes long. That avoids the race to set umask on the file…assuming that I check to make sure the umask on the directory actually worked and didn’t get hijacked on that race.

hmm, I think you are misunderstanding what umask is. umask is not set for a directory or a file; it is set for the current execution environment of a process, and it defines the mask used for files created subsequently. There is no race condition problem if you set the umask at the beginning of the process and create the file later. Check man umask for more info.

I somehow totally missed that when I read through the umask stuff I googled. Thanks for setting me right there. Obviously I’m new to this.

In that case, it sounds a little dangerous given that Prolog itself might be creating files on other threads, etc, right? Even if I get it, set it to what I want, and then reset it to the original there’s a chance I’m inadvertently changing permissions for whatever Prolog system file happens to get created at the time.

Don’t know how much of an issue that really is though.

@ericzinda

I keep thinking this should be much simpler.

So here is what I understand about the problem. Please correct me if this is wrong.

  1. You are on a Unix OS, e.g. Ubuntu.
  2. You are trying to communicate between two processes on the same physical computer. I infer this because you are using a file name instead of something like a URL. Is this correct that the communication is on the same physical machine?
  3. You are concerned about security, e.g. someone reading a physical file.
  4. You are new to Unix socket programming? I infer this because you say Obviously I’m new to this.

The reason I am asking this is that I found this which is for Linux. While I am also very new to Unix Domain Sockets, your problem of creating a secure socket seems like it would be a well visited and solved problem.

How to use unix domain socket without creating a socket file

The key word to search for is abstract socket address

HTH

Thanks. I don’t speak *nix as a native speaker and I’m definitely new to Unix Domain Sockets. I’ve got some experience with plain old sockets.

I had found the “abstract name” feature and was hopeful about it, but I avoided it because it appears to be a Linux-only feature (ref). I am hoping to have this code run wherever SWI Prolog can run. I assumed (perhaps wrongly) that SWI Prolog worked on POSIX compliant systems that weren’t Linux. Didn’t find much about that here.

That said, you make a good point. It might be worth restricting this feature to just Linux variants given the complexity. Hmmm…

1 Like

I would go with that if it were my problem. But I do know that many users of SWI-Prolog like to use Macs and that I have no info on, I am not a Mac person.

If someone comes along who needs the non-Linux solution odds are they will have some technical experience in that area and can help.

One of super nice facts about SWI-Prolog is that Jan W. is awesome at typically getting such changes into the GitHub repository within a day or two once the problem can be reproduced or the technical documentation can be supplied, e.g. my Docker API example, Jan W. had it working correctly and in the GitHub repository before I could finish comprehending the technical specs.

Right.

Typically a process creates a unix domain socket at a global well known
location in a directory that can only be written by trusted users. If you want to allow multiple server/client pairs that does not work. A good location could be in the persoanal directories (~/.local/share/swi-prolog/...), though the 92 char limit might be approached there (mostly ok still). Alternatively, create a directory in /tmp, give it access 700 (rwx------) and create your socket there. Now there are no race conditions and the socket is only accessible by the user.

Thanks for all the feedback and suggestions!

Here’s where I landed: The user can ask the system to create the socket file for them (recommended) or provide one themselves (with docs on the issues to consider). I need the second option because finding an appropriate tmp directory that works across all *nix systems including Mac is a rats nest. There needs to be a fallback option if the tmp directory is not called “/tmp” and this also provides flexibility if they want a specific file and folder for some reason:


% Creates a Unix Domain Socket file
% Requirements for this file are:
%    - The Prolog process will attempt to create and, if Prolog exits cleanly, 
%           delete this file when the server closes.  This means the directory 
%           must have the appropriate permissions to allow the Prolog process 
%           to do so.
%    - For security reasons, the filename should not be predictable and the 
%           directory it is contained in should have permissions set so that files 
%           created are only accessible to the current user.
%    - The path must be below 92 *bytes* long (including null terminator) to 
%           be portable according to the Linux documentation
%
% Creates a temporary subdirectory and temporary file in "/tmp".  If this 
%      directory doesn't exist, fails. 
% Need to call shell/1 as make_directory/1 doesn't allow setting permissions
% Need mkdir /tmp as opposed to one of the mktemp variants, as those work 
%     differently across mac and other linux and can generate large paths 
%     (and also don't allow setting permissions)
% Make a 38 byte pseudo random directory name using uuid
% Create with 700 (rwx------)  permission so it is only accessible by current user
% Create a secure tmp file in the new directory
% {set,current}_prolog_flag is copied to a thread, so
% no need to use a mutex.
% Close the stream so sockets can use it
unix_domain_socket_path(Created_Directory, Absolute_File_Path) :-
    uuid(UUID, [format(integer)]),
    format(atom(Created_Directory), '/tmp/~d', [UUID]),
    format(atom(Make_Directory_Command), 'mkdir -m 700 ~s', [Created_Directory]),
    shell(Make_Directory_Command),
    setup_call_cleanup( (   current_prolog_flag(tmp_dir, Save_Tmp_Dir),
                            set_prolog_flag(tmp_dir, Created_Directory)
                        ),
                        tmp_file_stream(Absolute_File_Path, Stream, []),
                        set_prolog_flag(tmp_dir, Save_Tmp_Dir)
                      ),
    close(Stream).

% Deletes either the created tmp directory and
%     file (if nonvar(Unix_Domain_Socket_Path))
% Or just the user provided file (otherwise)
delete_unix_domain_socket_file(Unix_Domain_Socket_Path, 
                               Unix_Domain_Socket_Path_And_File) :-
    (   nonvar(Unix_Domain_Socket_Path)
    ->  catch(delete_directory_and_contents(Unix_Domain_Socket_Path), error(_, _), true)
    ;   (   nonvar(Unix_Domain_Socket_Path_And_File)
        ->  catch(delete_file(Unix_Domain_Socket_Path_And_File), error(_, _), true)
        ;   true
        )
    ).

library(filesex): Extended operations on files

has chmod/2

Set the mode of the target file. Spec is one of +Mode , -Mode or a plain Mode, which adds new permissions, revokes permissions or sets the exact permissions. Mode itself is an integer, a POSIX mode name or a list of POSIX mode names. Defines names are suid , sgid , svtx and all names defined by the regular expression [ugo]*[rwx]* . Specifying none of “ugo” is the same as specifying all of them. For example, to make a file executable for the owner (user) and group, we can use:

?- chmod(myfile, +ugx).

Is that what you needed.

I only know of these things because of doing it the hard way then learning about them later. :slightly_smiling_face: