Skip to content
Advertisement

Towards understanding availability of xdg-open

I want to open an image, and in Windows I do:

#include <windows.h>
..
ShellExecute(NULL, "open", "https://gsamaras.files.wordpress.com/2018/11/chronosgod.png", NULL, NULL, SW_SHOWNORMAL);

I would like to use a Linux approach, where it’s so much easier to run something on the fly. Example:

char s[100];
snprintf(s, sizeof s, "%s %s", "xdg-open", "https://gsamaras.files.wordpress.com/2018/11/chronosgod.png");
system(s);

In my Ubuntu, it works. However, when running that in Wandbox (Live Demo), or in any other online compiler, I would most likely get an error:

sh: 1: xdg-open: not found

despite the fact that these online compilers seem to live in Linux (checked). I don’t expect the online compiler to open a browser for me, but I did expect the code to run without an error. Ah, and forget Mac (personal laptop, limiting my machines).

Since I have no other Linux machine to check, my question is: Can I expect that this code will work in most of the major Linux distributions?

Maybe the fact that it failed on online compilers is misleading.


PS: This is for my post on God of Time, so no worries about security.

Advertisement

Answer

Although Antti Haapala already completely answered the question, I thought some comments about the approach, and an example function making safe use trivial, might be useful.


xdg-open is part of desktop integration utilities from freedesktop.org, as part of the Portland project. One can expect them to be available on any computer running a desktop environment participating in freedesktop.org. This includes GNOME, KDE, and Xfce.

Simply put, this is the recommended way of opening a resource (be it a file or URL) when a desktop environment is in use, in whatever application the user prefers.

If there is no desktop environment in use, then there is no reason to expect xdg-open to be available either.


For Linux, I would suggest using a dedicated function, perhaps along the following lines. First, a couple of internal helper functions:

#define  _POSIX_C_SOURCE  200809L
#define  _GNU_SOURCE
//
// SPDX-License-Identifier: CC0-1.0
//
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <dirent.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

/* Number of bits in an unsigned long. */
#define  ULONG_BITS  (CHAR_BIT * sizeof (unsigned long))

/* Helper function to open /dev/null to a specific descriptor.
*/
static inline int devnullfd(const int fd)
{
    int  tempfd;

    /* Sanity check. */
    if (fd == -1)
        return errno = EINVAL;

    do {
        tempfd = open("/dev/null", O_RDWR | O_NOCTTY);
    } while (tempfd == -1 && errno == EINTR);
    if (tempfd == -1)
        return errno;

    if (tempfd != fd) {
        if (dup2(tempfd, fd) == -1) {
            const int  saved_errno = errno;
            close(tempfd);
            return errno = saved_errno;
        }
        if (close(tempfd) == -1)
            return errno;
    }

    return 0;
}

/* Helper function to close all except small descriptors
   specified in the mask. For obvious reasons, this is not
   thread safe, and is only intended to be used in recently
   forked child processes. */
static void closeall(const unsigned long  mask)
{
    DIR           *dir;
    struct dirent *ent;
    int            dfd;

    dir = opendir("/proc/self/fd/");
    if (!dir) {
        /* Cannot list open descriptors.  Just try and close all. */
        const long  fd_max = sysconf(_SC_OPEN_MAX);
        long        fd;

        for (fd = 0; fd < ULONG_BITS; fd++)
            if (!(mask & (1uL << fd)))
                close(fd);

        for (fd = ULONG_BITS; fd <= fd_max; fd++)
            close(fd);

        return;
    }

    dfd = dirfd(dir);

    while ((ent = readdir(dir)))
        if (ent->d_name[0] >= '0' && ent->d_name[0] <= '9') {
            const char *p = &ent->d_name[1];
            int         fd = ent->d_name[0] - '0';

            while (*p >= '0' && *p <= '9')
                fd = (10 * fd) + *(p++) - '0';

            if (*p)
                continue;

            if (fd == dfd)
                continue;

            if (fd < ULONG_MAX && (mask & (1uL << fd)))
                continue;

            close(fd);
        }

    closedir(dir);
}

closeall(0) tries hard to close all open file descriptors, and devnullfd(fd) tries to open fd to /dev/null. These are used to make sure that even if the user spoofs xdg-open, no file descriptors are leaked; only the file name or URL is passed.

On non-Linux POSIXy systems, you can replace them with something more suitable. On BSDs, use closefrom(), and handle the first ULONG_MAX descriptors in a loop.

The xdg_open(file-or-url) function itself is something along the lines of

/* Launch the user-preferred application to open a file or URL.
   Returns 0 if success, an errno error code otherwise.
*/ 
int xdg_open(const char *file_or_url)
{
    pid_t  child, p;
    int    status;

    /* Sanity check. */
    if (!file_or_url || !*file_or_url)
        return errno = EINVAL;

    /* Fork the child process. */
    child = fork();
    if (child == -1)
        return errno;
    else
    if (!child) {
        /* Child process. */

        uid_t  uid = getuid();  /* Real, not effective, user. */
        gid_t  gid = getgid();  /* Real, not effective, group. */

        /* Close all open file descriptors. */
        closeall(0);

        /* Redirect standard streams, if possible. */
        devnullfd(STDIN_FILENO);
        devnullfd(STDOUT_FILENO);
        devnullfd(STDERR_FILENO);

        /* Drop elevated privileges, if any. */
        if (setresgid(gid, gid, gid) == -1 ||
            setresuid(uid, uid, uid) == -1)
            _Exit(98);

        /* Have the child process execute in a new process group. */
        setsid();

        /* Execute xdg-open. */
        execlp("xdg-open", "xdg-open", file_or_url, (char *)0);

        /* Failed. xdg-open uses 0-5, we return 99. */
        _Exit(99);
    }

    /* Reap the child. */
    do {
        status = 0;
        p = waitpid(child, &status, 0);
    } while (p == -1 && errno == EINTR);
    if (p == -1)
        return errno;

    if (!WIFEXITED(status)) {
        /* Killed by a signal. Best we can do is I/O error, I think. */
        return errno = EIO;
    }

    switch (WEXITSTATUS(status)) {
    case 0: /* No error. */
        return errno = 0; /* It is unusual, but robust to explicitly clear errno. */
    case 1: /* Error in command line syntax. */
        return errno = EINVAL;      /* Invalid argument */
    case 2: /* File does not exist. */
        return errno = ENOENT;      /* No such file or directory */
    case 3: /* A required tool could not be found. */
        return errno = ENOSYS;      /* Not implemented */
    case 4: /* Action failed. */
        return errno = EPROTO;      /* Protocol error */
    case 98: /* Identity shenanigans. */
        return errno = EACCES;      /* Permission denied */
    case 99: /* xdg-open does not exist. */
        return errno = ENOPKG;      /* Package not installed */
    default:
        /* None of the other values should occur. */
        return errno = ENOSYS;      /* Not implemented */
    }
}

As already mentioned, it tries hard to close all open file descriptors, redirects the standard streams to /dev/null, ensures the effective and real identity matches (in case this is used in a setuid binary), and passes success/failure using the child process exit status.

The setresuid() and setresgid() calls are only available on OSes that have saved user and group ids. On others, use seteuid(uid) and setegid() instead.

This implementation tries to balance user configurability with security. Users can set the PATH so that their favourite xdg-open gets executed, but the function tries to ensure that no sensitive information or privileges are leaked to that process.

(Environment variables could be filtered, but they should not contain sensitive information in the first place, and we don’t really know which ones a desktop environment uses. So better not mess with them, to keep user surprises to a minimum.)

As a minimal test main(), try the following:

int main(int argc, char *argv[])
{
    int  arg, status;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "n");
        fprintf(stderr, "Usage: %s [ -h | --help ]n", argv[0]);
        fprintf(stderr, "       %s FILE-OR-URL ...n", argv[0]);
        fprintf(stderr, "n");
        fprintf(stderr, "This example program opens each specified file or URLn");
        fprintf(stderr, "xdg-open(1), and outputs success or failure for each.n");
        fprintf(stderr, "n");
        return EXIT_SUCCESS;
    }

    status = EXIT_SUCCESS;

    for (arg = 1; arg < argc; arg++)
        if (xdg_open(argv[arg])) {
            printf("%s: %s.n", argv[arg], strerror(errno));
            status = EXIT_FAILURE;
        } else
            printf("%s: Opened.n", argv[arg]);

    return status;
}

As the SPDX license identifier states, this example code is licensed under Creative Commons Zero 1.0. Use it any way you wish, in any code you want.

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement