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.