/* * consoles.c Routines to detect the system consoles * * Copyright (c) 2011 SuSE LINUX Products GmbH, All rights reserved. * Copyright (C) 2012 Karel Zak * Copyright (C) 2012 Werner Fink * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program (see the file COPYING); if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, * MA 02110-1301, USA. * * Author: Werner Fink */ #include #include #include #include #include #include #include #ifdef __linux__ # include # include # include # include #endif #include #include #include #include #if defined(USE_SULOGIN_EMERGENCY_MOUNT) # include # ifndef MS_RELATIME # define MS_RELATIME (1<<21) # endif # ifndef MNT_DETACH # define MNT_DETACH 2 # endif #endif #include "c.h" #include "canonicalize.h" #include "sulogin-consoles.h" #if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) # ifndef typeof # define typeof __typeof__ # endif # ifndef restrict # define restrict __restrict__ # endif #endif #define alignof(type) ((sizeof(type)+(sizeof(void*)-1)) & ~(sizeof(void*)-1)) #define strsize(string) (strlen((string))+1) static int consoles_debug; #define DBG(x) do { \ if (consoles_debug) { \ fputs("consoles debug: ", stderr); \ x; \ } \ } while (0) static inline void __attribute__ ((__format__ (__printf__, 1, 2))) dbgprint(const char * const mesg, ...) { va_list ap; va_start(ap, mesg); vfprintf(stderr, mesg, ap); va_end(ap); fputc('\n', stderr); } #ifdef USE_SULOGIN_EMERGENCY_MOUNT /* * Make C library standard calls such like ttyname(3) work * even if the system does not show any of the standard * directories. */ static uint32_t emergency_flags; # define MNT_PROCFS 0x0001 # define MNT_DEVTMPFS 0x0002 void emergency_do_umounts(void) { if (emergency_flags & MNT_DEVTMPFS) umount2("/dev", MNT_DETACH); if (emergency_flags & MNT_PROCFS) umount2("/proc", MNT_DETACH); } void emergency_do_mounts(void) { struct stat rt, xt; if (emergency_flags) { emergency_flags = 0; return; } if (stat("/", &rt) != 0) { warn("cannot get file status of root file system\n"); return; } if (stat("/proc", &xt) == 0 && rt.st_dev == xt.st_dev && mount("proc", "/proc", "proc", MS_RELATIME, NULL) == 0) emergency_flags |= MNT_PROCFS; if (stat("/dev", &xt) == 0 && rt.st_dev == xt.st_dev && mount("devtmpfs", "/dev", "devtmpfs", MS_RELATIME, "mode=0755,nr_inodes=0") == 0) { emergency_flags |= MNT_DEVTMPFS; mknod("/dev/console", S_IFCHR|S_IRUSR|S_IWUSR, makedev(TTYAUX_MAJOR, 1)); if (symlink("/proc/self/fd", "/dev/fd") == 0) { ignore_result( symlink("fd/0", "/dev/stdin") ); ignore_result( symlink("fd/1", "/dev/stdout") ); ignore_result( symlink("fd/2", "/dev/stderr") ); } } } #else /* !USE_SULOGIN_EMERGENCY_MOUNT */ void emergency_do_umounts(void) { } void emergency_do_mounts(void) { } #endif /* USE_SULOGIN_EMERGENCY_MOUNT */ /* * Read and allocate one line from file, * the caller has to free the result */ static __attribute__((__nonnull__)) char *oneline(const char * const file) { FILE *fp; char *ret = NULL; size_t dummy = 0; ssize_t len; DBG(dbgprint("reading %s", file)); if (!(fp = fopen(file, "r" UL_CLOEXECSTR))) return NULL; len = getline(&ret, &dummy, fp); if (len >= 0) { char *nl; if (len) ret[len-1] = '\0'; if ((nl = strchr(ret, '\n'))) *nl = '\0'; } fclose(fp); return ret; } #ifdef __linux__ /* * Read and determine active attribute for tty below * /sys/class/tty, the caller has to free the result. */ static __attribute__((__malloc__)) char *actattr(const char * const tty) { char *ret, *path; if (!tty || !*tty) return NULL; if (asprintf(&path, "/sys/class/tty/%s/active", tty) < 0) return NULL; ret = oneline(path); free(path); return ret; } /* * Read and determine device attribute for tty below * /sys/class/tty. */ static dev_t devattr(const char * const tty) { dev_t dev = 0; char *path, *value; if (!tty || !*tty) return 0; if (asprintf(&path, "/sys/class/tty/%s/dev", tty) < 0) return 0; value = oneline(path); if (value) { unsigned int maj, min; if (sscanf(value, "%u:%u", &maj, &min) == 2) dev = makedev(maj, min); free(value); } free(path); return dev; } #endif /* __linux__ */ /* * Search below /dev for the character device in `dev_t comparedev' variable. * Note that realpath(3) is used here to avoid not existent devices due the * strdup(3) used in our canonicalize_path()! */ static #ifdef __GNUC__ __attribute__((__nonnull__,__malloc__,__hot__)) #endif char* scandev(DIR *dir, const dev_t comparedev) { char path[PATH_MAX]; char *name = NULL; const struct dirent *dent; int len, fd; DBG(dbgprint("scanning /dev for %u:%u", major(comparedev), minor(comparedev))); /* * Try udev links on character devices first. */ if ((len = snprintf(path, sizeof(path), "/dev/char/%u:%u", major(comparedev), minor(comparedev))) > 0 && (size_t)len < sizeof(path)) { name = realpath(path, NULL); if (name) goto out; } fd = dirfd(dir); rewinddir(dir); while ((dent = readdir(dir))) { struct stat st; #ifdef _DIRENT_HAVE_D_TYPE if (dent->d_type != DT_UNKNOWN && dent->d_type != DT_CHR) continue; #endif if (fstatat(fd, dent->d_name, &st, 0) < 0) continue; if (!S_ISCHR(st.st_mode)) continue; if (comparedev != st.st_rdev) continue; if ((len = snprintf(path, sizeof(path), "/dev/%s", dent->d_name)) < 0 || (size_t)len >= sizeof(path)) continue; name = realpath(path, NULL); if (name) goto out; } #ifdef USE_SULOGIN_EMERGENCY_MOUNT /* * There was no /dev mounted hence and no device was found hence we create our own. */ if (!name && (emergency_flags & MNT_DEVTMPFS)) { if ((len = snprintf(path, sizeof(path), "/dev/tmp-%u:%u", major(comparedev), minor(comparedev))) < 0 || (size_t)len >= sizeof(path)) goto out; if (mknod(path, S_IFCHR|S_IRUSR|S_IWUSR, comparedev) < 0 && errno != EEXIST) goto out; name = realpath(path, NULL); } #endif out: return name; } /* * Default control characters for an unknown terminal line. */ /* * Allocate an aligned `struct console' memory area, * initialize its default values, and append it to * the global linked list. */ static #ifdef __GNUC__ __attribute__((__hot__)) #endif int append_console(struct list_head *consoles, const char * const name) { struct console *restrict tail; const struct console *last = NULL; DBG(dbgprint("appending %s", name)); if (!list_empty(consoles)) last = list_last_entry(consoles, struct console, entry); if (posix_memalign((void *) &tail, sizeof(void *), alignof(struct console) + strsize(name)) != 0) return -ENOMEM; INIT_LIST_HEAD(&tail->entry); INIT_CHARDATA(&tail->cp); list_add_tail(&tail->entry, consoles); tail->tty = ((char *) tail) + alignof(struct console); strcpy(tail->tty, name); tail->file = (FILE*)0; tail->flags = 0; tail->fd = -1; tail->id = last ? last->id + 1 : 0; tail->pid = -1; memset(&tail->tio, 0, sizeof(tail->tio)); return 0; } #ifdef __linux__ /* * return codes: * < 0 - fatal error (no mem or so... ) * 0 - success * 1 - recoverable error * 2 - detection not available */ static int detect_consoles_from_proc(struct list_head *consoles) { char fbuf[16 + 1]; DIR *dir = NULL; FILE *fc = NULL; int maj, min, rc = 1, matches; DBG(dbgprint("trying /proc")); fc = fopen("/proc/consoles", "r" UL_CLOEXECSTR); if (!fc) { rc = 2; goto done; } dir = opendir("/dev"); if (!dir) goto done; while ((matches = fscanf(fc, "%*s %*s (%16[^)]) %d:%d", fbuf, &maj, &min)) >= 1) { char *name; dev_t comparedev; if (matches != 3) continue; if (!strchr(fbuf, 'E')) continue; comparedev = makedev(maj, min); name = scandev(dir, comparedev); if (!name) continue; rc = append_console(consoles, name); free(name); if (rc < 0) goto done; } rc = list_empty(consoles) ? 1 : 0; done: if (dir) closedir(dir); if (fc) fclose(fc); DBG(dbgprint("[/proc rc=%d]", rc)); return rc; } /* * return codes: * < 0 - fatal error (no mem or so... ) * 0 - success * 1 - recoverable error * 2 - detection not available */ static int detect_consoles_from_sysfs(struct list_head *consoles) { char *attrib = NULL, *words, *token; DIR *dir = NULL; int rc = 1; DBG(dbgprint("trying /sys")); attrib = actattr("console"); if (!attrib) { rc = 2; goto done; } words = attrib; dir = opendir("/dev"); if (!dir) goto done; while ((token = strsep(&words, " \t\r\n"))) { char *name; dev_t comparedev; if (*token == '\0') continue; comparedev = devattr(token); if (comparedev == makedev(TTY_MAJOR, 0)) { char *tmp = actattr(token); if (!tmp) continue; comparedev = devattr(tmp); free(tmp); } name = scandev(dir, comparedev); if (!name) continue; rc = append_console(consoles, name); free(name); if (rc < 0) goto done; } rc = list_empty(consoles) ? 1 : 0; done: free(attrib); if (dir) closedir(dir); DBG(dbgprint("[/sys rc=%d]", rc)); return rc; } static int detect_consoles_from_cmdline(struct list_head *consoles) { char *cmdline, *words, *token; dev_t comparedev; DIR *dir = NULL; int rc = 1, fd; DBG(dbgprint("trying kernel cmdline")); cmdline = oneline("/proc/cmdline"); if (!cmdline) { rc = 2; goto done; } words= cmdline; dir = opendir("/dev"); if (!dir) goto done; while ((token = strsep(&words, " \t\r\n"))) { #ifdef TIOCGDEV unsigned int devnum; #else struct vt_stat vt; struct stat st; #endif char *colon, *name; if (*token != 'c') continue; if (strncmp(token, "console=", 8) != 0) continue; token += 8; if (strcmp(token, "brl") == 0) token += 4; if ((colon = strchr(token, ','))) *colon = '\0'; if (asprintf(&name, "/dev/%s", token) < 0) continue; if ((fd = open(name, O_RDWR|O_NONBLOCK|O_NOCTTY|O_CLOEXEC)) < 0) { free(name); continue; } free(name); #ifdef TIOCGDEV if (ioctl (fd, TIOCGDEV, &devnum) < 0) { close(fd); continue; } comparedev = (dev_t) devnum; #else if (fstat(fd, &st) < 0) { close(fd); continue; } comparedev = st.st_rdev; if (comparedev == makedev(TTY_MAJOR, 0)) { if (ioctl(fd, VT_GETSTATE, &vt) < 0) { close(fd); continue; } comparedev = makedev(TTY_MAJOR, (int)vt.v_active); } #endif close(fd); name = scandev(dir, comparedev); if (!name) continue; rc = append_console(consoles, name); free(name); if (rc < 0) goto done; } rc = list_empty(consoles) ? 1 : 0; done: if (dir) closedir(dir); free(cmdline); DBG(dbgprint("[kernel cmdline rc=%d]", rc)); return rc; } #ifdef TIOCGDEV static int detect_consoles_from_tiocgdev(struct list_head *consoles, const int fallback, const char *device) { unsigned int devnum; char *name; int rc = 1, fd = -1; dev_t comparedev; DIR *dir = NULL; struct console *console; DBG(dbgprint("trying tiocgdev")); if (!device || !*device) fd = dup(fallback); else fd = open(device, O_RDWR|O_NONBLOCK|O_NOCTTY|O_CLOEXEC); if (fd < 0) goto done; if (ioctl (fd, TIOCGDEV, &devnum) < 0) goto done; comparedev = (dev_t) devnum; dir = opendir("/dev"); if (!dir) goto done; name = scandev(dir, comparedev); closedir(dir); if (!name) { name = (char *) (device && *device ? device : ttyname(fallback)); if (!name) name = "/dev/tty1"; name = strdup(name); if (!name) { rc = -ENOMEM; goto done; } } rc = append_console(consoles, name); free(name); if (rc < 0) goto done; if (list_empty(consoles)) { rc = 1; goto done; } console = list_last_entry(consoles, struct console, entry); if (console && (!device || !*device)) console->fd = fallback; done: if (fd >= 0) close(fd); DBG(dbgprint("[tiocgdev rc=%d]", rc)); return rc; } #endif /* TIOCGDEV */ #endif /* __linux__ */ /* * Try to detect the real device(s) used for the system console * /dev/console if but only if /dev/console is used. On Linux * this can be more than one device, e.g. a serial line as well * as a virtual console as well as a simple printer. * * Returns 1 if stdout and stderr should be reconnected and 0 * otherwise or less than zero on error. */ int detect_consoles(const char *device, const int fallback, struct list_head *consoles) { int fd, reconnect = 0, rc; dev_t comparedev = 0; consoles_debug = getenv("CONSOLES_DEBUG") ? 1 : 0; if (!device || !*device) fd = fallback >= 0 ? dup(fallback) : - 1; else { fd = open(device, O_RDWR|O_NONBLOCK|O_NOCTTY|O_CLOEXEC); reconnect = 1; } DBG(dbgprint("detection started [device=%s, fallback=%d]", device, fallback)); if (fd >= 0) { DIR *dir; char *name; struct stat st; #ifdef TIOCGDEV unsigned int devnum; #endif #ifdef __GNU__ /* * The Hurd always gives st_rdev as 0, which causes this * method to select the first terminal it finds. */ close(fd); goto fallback; #endif DBG(dbgprint("trying device/fallback file descriptor")); if (fstat(fd, &st) < 0) { close(fd); goto fallback; } comparedev = st.st_rdev; if (reconnect && (fstat(fallback, &st) < 0 || comparedev != st.st_rdev)) dup2(fd, fallback); #ifdef __linux__ /* * Check if the device detection for Linux system console should be used. */ if (comparedev == makedev(TTYAUX_MAJOR, 0)) { /* /dev/tty */ close(fd); device = "/dev/tty"; goto fallback; } if (comparedev == makedev(TTYAUX_MAJOR, 1)) { /* /dev/console */ close(fd); goto console; } if (comparedev == makedev(TTYAUX_MAJOR, 2)) { /* /dev/ptmx */ close(fd); device = "/dev/tty"; goto fallback; } if (comparedev == makedev(TTY_MAJOR, 0)) { /* /dev/tty0 */ struct vt_stat vt; if (ioctl(fd, VT_GETSTATE, &vt) < 0) { close(fd); goto fallback; } comparedev = makedev(TTY_MAJOR, (int)vt.v_active); } #endif #ifdef TIOCGDEV if (ioctl (fd, TIOCGDEV, &devnum) < 0) { close(fd); goto fallback; } comparedev = (dev_t)devnum; #endif close(fd); dir = opendir("/dev"); if (!dir) goto fallback; name = scandev(dir, comparedev); closedir(dir); if (name) { rc = append_console(consoles, name); free(name); if (rc < 0) return rc; } if (list_empty(consoles)) goto fallback; DBG(dbgprint("detection success [rc=%d]", reconnect)); return reconnect; } #ifdef __linux__ console: /* * Detection of devices used for Linux system console using * the /proc/consoles API with kernel 2.6.38 and higher. */ rc = detect_consoles_from_proc(consoles); if (rc == 0) return reconnect; /* success */ if (rc < 0) return rc; /* fatal error */ /* * Detection of devices used for Linux system console using * the sysfs /sys/class/tty/ API with kernel 2.6.37 and higher. */ rc = detect_consoles_from_sysfs(consoles); if (rc == 0) return reconnect; /* success */ if (rc < 0) return rc; /* fatal error */ /* * Detection of devices used for Linux system console using * kernel parameter on the kernels command line. */ rc = detect_consoles_from_cmdline(consoles); if (rc == 0) return reconnect; /* success */ if (rc < 0) return rc; /* fatal error */ /* * Detection of the device used for Linux system console using * the ioctl TIOCGDEV if available (e.g. official 2.6.38). */ #ifdef TIOCGDEV rc = detect_consoles_from_tiocgdev(consoles, fallback, device); if (rc == 0) return reconnect; /* success */ if (rc < 0) return rc; /* fatal error */ #endif if (!list_empty(consoles)) { DBG(dbgprint("detection success [rc=%d]", reconnect)); return reconnect; } #endif /* __linux __ */ fallback: if (fallback >= 0) { const char *name; char *n; struct console *console; if (device && *device != '\0') name = device; else name = ttyname(fallback); if (!name) name = "/dev/tty"; n = strdup(name); if (!n) return -ENOMEM; rc = append_console(consoles, n); free(n); if (rc < 0) return rc; if (list_empty(consoles)) return 1; console = list_last_entry(consoles, struct console, entry); if (console) console->fd = fallback; } DBG(dbgprint("detection done by fallback [rc=%d]", reconnect)); return reconnect; } #ifdef TEST_PROGRAM int main(int argc, char *argv[]) { char *name = NULL; int fd, re; struct list_head *p, consoles; if (argc == 2) { name = argv[1]; fd = open(name, O_RDWR); } else { name = ttyname(STDIN_FILENO); fd = STDIN_FILENO; } if (!name) errx(EXIT_FAILURE, "usage: %s []\n", program_invocation_short_name); INIT_LIST_HEAD(&consoles); re = detect_consoles(name, fd, &consoles); list_for_each(p, &consoles) { struct console *c = list_entry(p, struct console, entry); printf("%s: id=%d %s\n", c->tty, c->id, re ? "(reconnect) " : ""); } return 0; } #endif