Optimal Default Path for XDG_RUNTIME_DIR: Secure Alternatives to /tmp When Undefined


3 views

While the XDG Base Directory Specification provides excellent defaults for most directories, XDG_RUNTIME_DIR remains conspicuously undefined in the spec. This creates challenges for developers building applications that require runtime files like named pipes or UNIX sockets.

The common approach of using /tmp/myserver-$USER has several limitations:


// Problematic implementation example
char *path = "/tmp/myapp-";
char *user = getenv("USER");
// Security risk: race condition during path creation

This violates three key requirements:

  • No automatic cleanup (files persist after logout)
  • Potential permission issues (world-writable directory)
  • No guaranteed exclusivity (other users might predict paths)

Modern Linux systems using systemd typically create /run/user/$UID with these characteristics:


$ ls -ld /run/user/$(id -u)
drwx------ 3 user user 60 Jan 01 00:00 /run/user/1000

Key properties:

  • 0700 permissions (user-exclusive)
  • Automatic deletion on logout
  • Sticky bit prevents tampering

Here's a recommended fallback implementation in C:


#include <sys/types.h>
#include <pwd.h>

const char *get_runtime_dir() {
    const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
    if (xdg_runtime) return xdg_runtime;

    // Fallback 1: Systemd convention
    struct passwd *pw = getpwuid(getuid());
    char *systemd_path = NULL;
    if (asprintf(&systemd_path, "/run/user/%d", getuid()) > 0) {
        if (access(systemd_path, F_OK) == 0) {
            return systemd_path;
        }
        free(systemd_path);
    }

    // Fallback 2: Secure /tmp alternative
    char *tmp_path = NULL;
    if (asprintf(&tmp_path, "/tmp/.%s-%.10s", 
                pw->pw_name, 
                crypt(pw->pw_name, "$6$somesalt$")) > 0) {
        mkdir(tmp_path, 0700);
        return tmp_path;
    }

    return "."; // Last resort
}

When implementing your own fallback:

  1. Always set 0700 permissions
  2. Use cryptographic hashing for directory names
  3. Implement proper cleanup hooks
  4. Consider using mkdtemp() for atomic creation

For Python applications, consider this wrapper:


import os
from pathlib import Path
import hashlib

def get_runtime_dir(appname: str) -> Path:
    if xdg := os.getenv("XDG_RUNTIME_DIR"):
        return Path(xdg)
    
    # Systemd fallback
    systemd_path = Path(f"/run/user/{os.getuid()}")
    if systemd_path.exists():
        return systemd_path
    
    # Secure /tmp fallback
    username = os.getenv("USER", "unknown")
    salt = "fixed-seed-for-consistency"
    hash = hashlib.sha256((username + salt).encode()).hexdigest()[:16]
    tmp_path = Path(f"/tmp/.{username}-{hash}")
    tmp_path.mkdir(mode=0o700, exist_ok=True)
    return tmp_path

The XDG Base Directory Specification provides excellent defaults for most directories, but leaves XDG_RUNTIME_DIR implementation as an exercise for developers. When creating named pipes or sockets in a client-server architecture, we need a location that satisfies three critical requirements:

1. User-specific isolation
2. Automatic cleanup on logout
3. Secure permissions (0700)

While /tmp/myserver-$USER might seem like a reasonable choice, it fails to meet the specification's requirements:

  • No automatic cleanup - files persist after logout
  • Potential security issues if not properly permissioned
  • Possible collisions with other applications

Modern Linux systems using systemd provide the ideal solution:

# Systemd creates this automatically for logged-in users
/run/user/$(id -u)

This directory:

  • Has strict 0700 permissions
  • Is automatically created/destroyed with user sessions
  • Is on tmpfs for security

Here's a robust implementation in C that handles all cases:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>

const char *get_runtime_dir() {
    // 1. Check XDG_RUNTIME_DIR first
    const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
    if (xdg_runtime != NULL) {
        return xdg_runtime;
    }

    // 2. Try systemd's /run/user/$UID
    char systemd_path[256];
    snprintf(systemd_path, sizeof(systemd_path), "/run/user/%d", getuid());
    if (access(systemd_path, F_OK) == 0) {
        return strdup(systemd_path);
    }

    // 3. Fallback to secure /tmp alternative
    const char *username = getpwuid(getuid())->pw_name;
    char *tmp_path = malloc(256);
    snprintf(tmp_path, 256, "/tmp/%s-runtime", username);
    
    // Ensure directory exists with correct permissions
    mkdir(tmp_path, 0700);
    return tmp_path;
}

For systems without systemd, consider using pam_systemd's behavior as inspiration:

  1. Create /run/user/$UID at login via PAM
  2. Set strict permissions (0700)
  3. Clean up at logout

A shell implementation might look like:

#!/bin/sh
RUNTIME_DIR="/run/user/$(id -u)"
if [ ! -d "$RUNTIME_DIR" ]; then
    mkdir -p "$RUNTIME_DIR"
    chmod 700 "$RUNTIME_DIR"
    chown "$(id -u):$(id -g)" "$RUNTIME_DIR"
fi
export XDG_RUNTIME_DIR="$RUNTIME_DIR"

When implementing your own runtime directory:

  • Always use 0700 permissions
  • Prefer tmpfs over disk storage
  • Validate all path components
  • Consider symbolic link attacks