diff --git a/tests/Regression/bz1841869-pwd-from-tty/runtest.sh b/tests/Regression/bz1841869-pwd-from-tty/runtest.sh new file mode 100755 index 0000000..7bc1c22 --- /dev/null +++ b/tests/Regression/bz1841869-pwd-from-tty/runtest.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# +# File: ./tests/Regression/bz1841869-pwd-from-tty/runtest.sh +# Author: Jiří Kučera +# Brief: Test whether the passphrase is read from tty properly +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2021 Red Hat, Inc. +# +# 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 of the License, 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; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +# Include Beaker environment +. /usr/share/beakerlib/beakerlib.sh || exit 1 + +TEST="${TEST:-/CoreOS/volume_key/tests/Regression/bz1841869-pwd-from-tty}" +TESTVERSION="${TESTVERSION:-1.0}" +PACKAGES="${PACKAGES:-volume_key}" +REQUIRES="${REQUIRES:-gcc cryptsetup nss-tools expect tcllib}" + +_PASSPHRASE="t0pS3cr31!" + +rlJournalStart + rlPhaseStartSetup + rlAssertRpm --all + rlRun "TmpDir=\$(mktemp -d)" 0 "Creating tmp directory" + rlRun "cp -v vk_wrap.c ${TmpDir}" 0 "Copying vk_wrap.c" + rlRun "cp -v vk_dummy.c ${TmpDir}" 0 "Copying vk_dummy.c" + rlRun "pushd ${TmpDir}" + rlRun "gcc --std=c99 -pedantic -W -Wall -Werror -o vk_wrap vk_wrap.c" 0 \ + "Compiling vk_wrap.c" + rlRun "gcc --std=c99 -pedantic -W -Wall -Werror -o vk_dummy vk_dummy.c" 0 \ + "Compiling vk_dummy.c" + rlPhaseEnd + + rlPhaseStartTest + _PARAMS="VK_WRAP_PROGAM=${TmpDir}/vk_dummy" + _PARAMS="${_PARAMS} VK_WRAP_PASSPHRASE=${_PASSPHRASE}" + rlRun "${_PARAMS} ${TmpDir}/vk_wrap" 0 "Run volume_key via wrapper" + rlPhaseEnd + + rlPhaseStartCleanup + rlRun "popd" + rlRun "rm -rfd $TmpDir" 0 "Removing tmp directory" + rlPhaseEnd +rlJournalPrintText +rlJournalEnd diff --git a/tests/Regression/bz1841869-pwd-from-tty/vk_dummy.c b/tests/Regression/bz1841869-pwd-from-tty/vk_dummy.c new file mode 100644 index 0000000..98e92e5 --- /dev/null +++ b/tests/Regression/bz1841869-pwd-from-tty/vk_dummy.c @@ -0,0 +1,185 @@ +/* + * File: ./tests/Regression/bz1841869-pwd-from-tty/vk_dummy.c + * Author: Jiří Kučera + * Brief: Dummy volume_key (only request passphrase) + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * Copyright (C) 2021 Red Hat, Inc. + * + * 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 of the License, 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; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +struct ditem { + char *message; + struct ditem *next; +}; + +struct diagnose { + struct ditem *head, *tail; +}; + +static int diagnose_append(struct diagnose *diagnose, char *message) +{ + struct ditem *p = NULL; + + if ((p = malloc(sizeof(struct ditem))) == NULL) + return -1; + p->message = message; + p->next = NULL; + + if (diagnose->head == NULL) + diagnose->head = diagnose->tail = p; + else { + diagnose->tail->next = p; + diagnose->tail = p; + } + + return 0; +} + +static void diagnose_dispose(struct diagnose *diagnose) +{ + struct ditem *p = NULL, *t = NULL; + + for (p = diagnose->head; (t = p); p = p->next, free(t)) + free(t->message); + diagnose->head = diagnose->tail = NULL; +} + +static void diagnose_show(struct diagnose *diagnose) +{ + struct ditem *p = NULL; + + fputs("=== Diagnose ===\n", stderr); + for (p = diagnose->head; p; p = p->next) + fprintf(sdterr, "- %s\n", p->message ? p->message : "(null)"); + fputs("================\n", stderr); +} + +static void log_read(int nread, struct diagnose *diagnose) +{ + char *s = NULL; + + if (nread < 0) { + switch (errno) { + case EINTR: + diagnose_append(diagnose, strdup("(EINTR)")); + break; + case EAGAIN: + diagnose_append(diagnose, strdup("(EAGAIN)")); + break; + default: + if (asprintf(&s, "(%s)", strerror(errno)) < 0) + s = NULL; + diagnose_append(diagnose, s); + break; + } + } + else if (nread == 0) + diagnose_append(diagnose, strdup("(EOF)")); + else { + if (asprintf(&s, "Read %d bytes.", nread) < 0) + s = NULL; + diagnose_append(diagnose, s); + } +} + +static char *get_password(const char *prompt, struct diagnose *diagnose) +{ + int tty = -1, in_file = -1, out_file = -1, echo_disabled = 0; + int nread = 0, tread = 0; + char buf[LINE_MAX + 1], *p = NULL; + struct termios otermios; + + if ((tty = open("/dev/tty", O_RDONLY, 0)) < 0) { + in_file = STDIN_FILENO; + out_file = STDERR_FILENO; + } + else + out_file = in_file = tty; + + write(out_file, prompt, strlen(prompt)); + + if (tcgetattr(in_file, &otermios) == 0) { + struct termios ntermios; + + ntermios = otermios; + ntermios.c_lflag &= ~ECHO; + echo_disabled = tcsetattr(in_file, TCSAFLUSH, &ntermios) == 0; + } + + while (tread < LINE_MAX) { + nread = read(in_file, buf + tread, LINE_MAX - tread); + log_read(nread, diagnose); + if (nread < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + break; + } + else if (nread == 0) + break; + tread += nread; + } + buf[tread] = '\0'; + + if (echo_disabled) { + tcsetattr(in_file, TCSAFLUSH, &otermios); + write(out_file, "\n", 1); + } + + if (tty >= 0) + close(tty); + + if (nread < 0) + return NULL; + + p = strchr(buf, '\r'); + if (p != NULL) + *p = '\0'; + p = strchr(buf, '\n'); + if (p != NULL) + *p = '\0'; + + return strdup(buf); +} + +int main(void) +{ + struct diagnose diagnose = {NULL, NULL}; + char *passphrase = NULL, *s = NULL; + + passphrase = get_password("Please, enter the passphrase: ", &diagnose); + if (asprintf(&s, "The passphrase is: %s", passphrase) < 0) + s = NULL; + free(passphrase); + diagnose_append(&diagnose, s); + diagnose_show(&diagnose); + diagnose_dispose(&diagnose); + return EXIT_SUCCESS; +} diff --git a/tests/Regression/bz1841869-pwd-from-tty/vk_wrap.c b/tests/Regression/bz1841869-pwd-from-tty/vk_wrap.c new file mode 100644 index 0000000..d41bcad --- /dev/null +++ b/tests/Regression/bz1841869-pwd-from-tty/vk_wrap.c @@ -0,0 +1,179 @@ +/* + * File: ./tests/Regression/bz1841869-pwd-from-tty/vk_wrap.c + * Author: Jiří Kučera + * Brief: Run volume_key, emulate EINTR and EAGAIN + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * Copyright (C) 2021 Red Hat, Inc. + * + * 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 of the License, 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; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +enum emultypes { + NO_EMUL, + EINTR_EMUL, + EAGAIN_EMUL +}; + +static void error(const char *format, ...) +{ + va_list args; + + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + +static char *get_env(const char *name, int optional) +{ + char *p = getenv(name); + + if (!optional && (p == NULL || *p == '\0')) + return (error("Environment variable %s is not set!\n", name), NULL); + return p; +} + +static int get_emul_type(void) +{ + char *p = get_env("VK_WRAP_EMULATE", 1); + + if (p == NULL) + return NO_EMUL; + else if (strcmp(p, "EINTR") == 0) + return EINTR_EMUL; + else if (strcmp(p, "EAGAIN") == 0) + return EAGAIN_EMUL; + return NO_EMUL; +} + +static void close_pipe(int pipefd[2]) +{ + close(pipefd[0]); + close(pipefd[1]); +} + +static void freeargs(char **args) +{ + char **p = args; + + if (!p) + return; + while (*p) + free(*p++); + free(args); +} + +static char **mkargs(const *program, int argc, char **argv) +{ + char **args = NULL; + size_t args_size = (argc + 1) * sizeof(*args); + int i = 0; + + if ((args = malloc(args_size)) == NULL) + return NULL; + memset(args, '\0', args_size); + if ((args[0] = strdup(program)) == NULL) + return (freeargs(args), NULL); + for (i = 1; i < argc; i++) + if ((args[i] = strdup(argv[i])) == NULL) + return (freeargs(args), NULL); + return args; +} + +int main(int argc, char *argv[]) +{ + char *program = NULL, *passphrase = NULL; + int emultype = NO_EMUL, cpid = 0, pipefd[2]; + + if ((program = get_env("VK_WRAP_PROGRAM", 0)) == NULL) + return EXIT_FAILURE; + if ((passphrase = get_env("VK_WRAP_PASSPHRASE", 0)) == NULL) + return EXIT_FAILURE; + emultype = get_emul_type(); + + if (pipe(pipefd) < 0) + return (perror("pipe"), EXIT_FAILURE); + + /* Set non-blocking reading only if we emulate EAGAIN */ + if (emultype == EAGAIN_EMUL && fcntl(pipefd[0], F_SETFL, O_NONBLOCK) < 0) + return (perror("fcntl"), close_pipe(pipefd), EXIT_FAILURE); + + cpid = fork(); + if (cpid < 0) + return (perror("fork"), close_pipe(pipefd), EXIT_FAILURE); + + if (cpid == 0) { + char **args = NULL; + + /* We are in the child, close the writing end of the pipe */ + close(pipefd[1]); + /* Map the reading end of the pipe to standard input */ + if (pipefd[0] != STDIN_FILENO) { + if (dup2(pipefd[0], STDIN_FILENO) < 0) + return (perror("dup2"), close(pipefd[0]), EXIT_FAILURE); + close(pipefd[0]); + } + /* Copy the program arguments */ + if ((args = mkargs(program, argc, argv)) == NULL) + return (perror("mkargs"), EXIT_FAILURE); + /* Replace the current process image with the program */ + if (execv(args[0], args) < 0) + return (perror("execv"), freeargs(args), EXIT_FAILURE); + } + + /* We are in the parent, close the reading end of the pipe */ + close(pipefd[0]); + switch (emultype) { + case EINTR_EMUL: + /* Hopefully this emulate EINTR */ + sleep(2); /* Wait for the child to enter read */ + /* Now child is inside kernel space, send it the signal */ + kill(cpid, SIGSTOP); + sleep(1); /* Wait a bit */ + /* Resume child */ + kill(cpid, SIGCONT); + break; + case EAGAIN_EMUL: + /* Delay writing the passphrase. When the child read from an empty pipe + in the non-blocking mode, read() returns -1 and sets errno to EAGAIN + */ + sleep(3); + break; + default: + break; + } + /* Write the passphrase */ + if (write(pipefd[1], passphrase, strlen(passphrase)) < 0) + perror("write"); + if (write(pipefd[1], "\n", 1) < 0) + perror("write"); + close(pipefd[1]); /* Reader will see EOF */ + /* Wait for the child to finish */ + waitpid(cpid, NULL, 0); + return EXIT_SUCCESS; +}