/******************************************************************************
* A module for Linux-PAM that will cache authentication results, inspired by
* (and implemented with an eye toward being mixable with) sudo.
*
* Copyright (c) 2002 Red Hat, Inc.
* Written by Nalin Dahyabhai <nalin@redhat.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, and the entire permission notice in its entirety,
* including the disclaimer of warranties.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* ALTERNATIVELY, this product may be distributed under the terms of
* the GNU Public License, in which case the provisions of the GPL are
* required INSTEAD OF the above restrictions. (This clause is
* necessary due to a potential bad interaction between the GPL and
* the restrictions contained in a BSD-style copyright.)
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
#define PAM_SM_AUTH
#define PAM_SM_SESSION
#include "config.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <utmp.h>
#include <syslog.h>
#include <paths.h>
#include "hmacsha1.h"
#include <security/pam_modules.h>
#include <security/_pam_macros.h>
#include <security/pam_ext.h>
#include <security/pam_modutil.h>
/* The default timeout we use is 5 minutes, which matches the sudo default
* for the timestamp_timeout parameter. */
#define DEFAULT_TIMESTAMP_TIMEOUT (5 * 60)
#define MODULE "pam_timestamp"
#define TIMESTAMPDIR _PATH_VARRUN "/" MODULE
#define TIMESTAMPKEY TIMESTAMPDIR "/_pam_timestamp_key"
/* Various buffers we use need to be at least as large as either PATH_MAX or
* LINE_MAX, so choose the larger of the two. */
#if (LINE_MAX > PATH_MAX)
#define BUFLEN LINE_MAX
#else
#define BUFLEN PATH_MAX
#endif
/* Return PAM_SUCCESS if the given directory looks "safe". */
static int
check_dir_perms(pam_handle_t *pamh, const char *tdir)
{
char scratch[BUFLEN];
struct stat st;
int i;
/* Check that the directory is "safe". */
if ((tdir == NULL) || (strlen(tdir) == 0)) {
return PAM_AUTH_ERR;
}
/* Iterate over the path, checking intermediate directories. */
memset(scratch, 0, sizeof(scratch));
for (i = 0; (tdir[i] != '\0') && (i < (int)sizeof(scratch)); i++) {
scratch[i] = tdir[i];
if ((scratch[i] == '/') || (tdir[i + 1] == '\0')) {
/* We now have the name of a directory in the path, so
* we need to check it. */
if ((lstat(scratch, &st) == -1) && (errno != ENOENT)) {
pam_syslog(pamh, LOG_ERR,
"unable to read `%s': %m",
scratch);
return PAM_AUTH_ERR;
}
if (!S_ISDIR(st.st_mode)) {
pam_syslog(pamh, LOG_ERR,
"`%s' is not a directory",
scratch);
return PAM_AUTH_ERR;
}
if (S_ISLNK(st.st_mode)) {
pam_syslog(pamh, LOG_ERR,
"`%s' is a symbolic link",
scratch);
return PAM_AUTH_ERR;
}
if (st.st_uid != 0) {
pam_syslog(pamh, LOG_ERR,
"`%s' owner UID != 0",
scratch);
return PAM_AUTH_ERR;
}
if (st.st_gid != 0) {
pam_syslog(pamh, LOG_ERR,
"`%s' owner GID != 0",
scratch);
return PAM_AUTH_ERR;
}
if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
pam_syslog(pamh, LOG_ERR,
"`%s' permissions are lax",
scratch);
return PAM_AUTH_ERR;
}
}
}
return PAM_SUCCESS;
}
/* Validate a tty pathname as actually belonging to a tty, and return its base
* name if it's valid. */
static const char *
check_tty(const char *tty)
{
/* Check that we're not being set up to take a fall. */
if ((tty == NULL) || (strlen(tty) == 0)) {
return NULL;
}
/* Pull out the meaningful part of the tty's name. */
if (strchr(tty, '/') != NULL) {
if (strncmp(tty, "/dev/", 5) != 0) {
/* Make sure the device node is actually in /dev/,
* noted by Michal Zalewski. */
return NULL;
}
tty = strrchr(tty, '/') + 1;
}
/* Make sure the tty wasn't actually a directory (no basename). */
if (!strlen(tty) || !strcmp(tty, ".") || !strcmp(tty, "..")) {
return NULL;
}
return tty;
}
/* Determine the right path name for a given user's timestamp. */
static int
format_timestamp_name(char *path, size_t len,
const char *timestamp_dir,
const char *tty,
const char *ruser,
const char *user)
{
if (strcmp(ruser, user) == 0) {
return snprintf(path, len, "%s/%s/%s", timestamp_dir,
ruser, tty);
} else {
return snprintf(path, len, "%s/%s/%s:%s", timestamp_dir,
ruser, tty, user);
}
}
/* Check if a given timestamp date, when compared to a current time, fits
* within the given interval. */
static int
timestamp_good(time_t then, time_t now, time_t interval)
{
if (((now >= then) && ((now - then) < interval)) ||
((now < then) && ((then - now) < (2 * interval)))) {
return PAM_SUCCESS;
}
return PAM_AUTH_ERR;
}
static int
check_login_time(const char *ruser, time_t timestamp)
{
struct utmp utbuf, *ut;
time_t oldest_login = 0;
setutent();
while(
#ifdef HAVE_GETUTENT_R
!getutent_r(&utbuf, &ut)
#else
(ut = getutent()) != NULL
#endif
) {
if (ut->ut_type != USER_PROCESS) {
continue;
}
if (strncmp(ruser, ut->ut_user, sizeof(ut->ut_user)) != 0) {
continue;
}
if (oldest_login == 0 || oldest_login > ut->ut_tv.tv_sec) {
oldest_login = ut->ut_tv.tv_sec;
}
}
endutent();
if(oldest_login == 0 || timestamp < oldest_login) {
return PAM_AUTH_ERR;
}
return PAM_SUCCESS;
}
#ifndef PAM_TIMESTAMP_MAIN
static int
get_ruser(pam_handle_t *pamh, char *ruserbuf, size_t ruserbuflen)
{
const void *ruser;
struct passwd *pwd;
if (ruserbuf == NULL || ruserbuflen < 1)
return -2;
/* Get the name of the source user. */
if (pam_get_item(pamh, PAM_RUSER, &ruser) != PAM_SUCCESS) {
ruser = NULL;
}
if ((ruser == NULL) || (strlen(ruser) == 0)) {
/* Barring that, use the current RUID. */
pwd = pam_modutil_getpwuid(pamh, getuid());
if (pwd != NULL) {
ruser = pwd->pw_name;
}
} else {
/*
* This ruser is used by format_timestamp_name as a component
* of constructed timestamp pathname, so ".", "..", and '/'
* are disallowed to avoid potential path traversal issues.
*/
if (!strcmp(ruser, ".") ||
!strcmp(ruser, "..") ||
strchr(ruser, '/')) {
ruser = NULL;
}
}
if (ruser == NULL || strlen(ruser) >= ruserbuflen) {
*ruserbuf = '\0';
return -1;
}
strcpy(ruserbuf, ruser);
return 0;
}
/* Get the path to the timestamp to use. */
static int
get_timestamp_name(pam_handle_t *pamh, int argc, const char **argv,
char *path, size_t len)
{
const char *user, *tty;
const void *void_tty;
const char *tdir = TIMESTAMPDIR;
char ruser[BUFLEN];
int i, debug = 0;
/* Parse arguments. */
for (i = 0; i < argc; i++) {
if (strcmp(argv[i], "debug") == 0) {
debug = 1;
}
}
for (i = 0; i < argc; i++) {
if (strncmp(argv[i], "timestampdir=", 13) == 0) {
tdir = argv[i] + 13;
if (debug) {
pam_syslog(pamh, LOG_DEBUG,
"storing timestamps in `%s'",
tdir);
}
}
}
i = check_dir_perms(pamh, tdir);
if (i != PAM_SUCCESS) {
return i;
}
/* Get the name of the target user. */
if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS) {
user = NULL;
}
if ((user == NULL) || (strlen(user) == 0)) {
return PAM_AUTH_ERR;
}
if (debug) {
pam_syslog(pamh, LOG_DEBUG, "becoming user `%s'", user);
}
/* Get the name of the source user. */
if (get_ruser(pamh, ruser, sizeof(ruser)) || strlen(ruser) == 0) {
return PAM_AUTH_ERR;
}
if (debug) {
pam_syslog(pamh, LOG_DEBUG, "currently user `%s'", ruser);
}
/* Get the name of the terminal. */
if (pam_get_item(pamh, PAM_TTY, &void_tty) != PAM_SUCCESS) {
tty = NULL;
} else {
tty = void_tty;
}
if ((tty == NULL) || (strlen(tty) == 0)) {
tty = ttyname(STDIN_FILENO);
if ((tty == NULL) || (strlen(tty) == 0)) {
tty = ttyname(STDOUT_FILENO);
}
if ((tty == NULL) || (strlen(tty) == 0)) {
tty = ttyname(STDERR_FILENO);
}
if ((tty == NULL) || (strlen(tty) == 0)) {
/* Match sudo's behavior for this case. */
tty = "unknown";
}
}
if (debug) {
pam_syslog(pamh, LOG_DEBUG, "tty is `%s'", tty);
}
/* Snip off all but the last part of the tty name. */
tty = check_tty(tty);
if (tty == NULL) {
return PAM_AUTH_ERR;
}
/* Generate the name of the file used to cache auth results. These
* paths should jive with sudo's per-tty naming scheme. */
if (format_timestamp_name(path, len, tdir, tty, ruser, user) >= (int)len) {
return PAM_AUTH_ERR;
}
if (debug) {
pam_syslog(pamh, LOG_DEBUG, "using timestamp file `%s'", path);
}
return PAM_SUCCESS;
}
/* Tell the user that access has been granted. */
static void
verbose_success(pam_handle_t *pamh, long diff)
{
pam_info(pamh, _("Access granted (last access was %ld seconds ago)."), diff);
}
int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
struct stat st;
time_t interval = DEFAULT_TIMESTAMP_TIMEOUT;
int i, fd, debug = 0, verbose = 0;
char path[BUFLEN], *p, *message, *message_end;
long tmp;
const void *void_service;
const char *service;
time_t now, then;
/* Parse arguments. */
for (i = 0; i < argc; i++) {
if (strcmp(argv[i], "debug") == 0) {
debug = 1;
}
}
for (i = 0; i < argc; i++) {
if (strncmp(argv[i], "timestamp_timeout=", 18) == 0) {
tmp = strtol(argv[i] + 18, &p, 0);
if ((p != NULL) && (*p == '\0')) {
interval = tmp;
if (debug) {
pam_syslog(pamh, LOG_DEBUG,
"setting timeout to %ld"
" seconds", (long)interval);
}
}
} else
if (strcmp(argv[i], "verbose") == 0) {
verbose = 1;
if (debug) {
pam_syslog(pamh, LOG_DEBUG,
"becoming more verbose");
}
}
}
if (flags & PAM_SILENT) {
verbose = 0;
}
/* Get the name of the timestamp file. */
if (get_timestamp_name(pamh, argc, argv,
path, sizeof(path)) != PAM_SUCCESS) {
return PAM_AUTH_ERR;
}
/* Get the name of the service. */
if (pam_get_item(pamh, PAM_SERVICE, &void_service) != PAM_SUCCESS) {
service = NULL;
} else {
service = void_service;
}
if ((service == NULL) || (strlen(service) == 0)) {
service = "(unknown)";
}
/* Open the timestamp file. */
fd = open(path, O_RDONLY | O_NOFOLLOW);
if (fd == -1) {
if (debug) {
pam_syslog(pamh, LOG_DEBUG,
"cannot open timestamp `%s': %m",
path);
}
return PAM_AUTH_ERR;
}
if (fstat(fd, &st) == 0) {
int count;
void *mac;
size_t maclen;
char ruser[BUFLEN];
/* Check that the file is owned by the superuser. */
if ((st.st_uid != 0) || (st.st_gid != 0)) {
pam_syslog(pamh, LOG_ERR, "timestamp file `%s' is "
"not owned by root", path);
close(fd);
return PAM_AUTH_ERR;
}
/* Check that the file is a normal file. */
if (!(S_ISREG(st.st_mode))) {
pam_syslog(pamh, LOG_ERR, "timestamp file `%s' is "
"not a regular file", path);
close(fd);
return PAM_AUTH_ERR;
}
/* Check that the file is the expected size. */
if (st.st_size == 0) {
/* Invalid, but may have been created by sudo. */
close(fd);
return PAM_AUTH_ERR;
}
if (st.st_size !=
(off_t)(strlen(path) + 1 + sizeof(then) + hmac_sha1_size())) {
pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' "
"appears to be corrupted", path);
close(fd);
return PAM_AUTH_ERR;
}
/* Read the file contents. */
message = malloc(st.st_size);
count = 0;
if (!message) {
close(fd);
return PAM_BUF_ERR;
}
while (count < st.st_size) {
i = read(fd, message + count, st.st_size - count);
if ((i == 0) || (i == -1)) {
break;
}
count += i;
}
if (count < st.st_size) {
pam_syslog(pamh, LOG_NOTICE, "error reading timestamp "
"file `%s': %m", path);
close(fd);
free(message);
return PAM_AUTH_ERR;
}
message_end = message + strlen(path) + 1 + sizeof(then);
/* Regenerate the MAC. */
hmac_sha1_generate_file(pamh, &mac, &maclen, TIMESTAMPKEY, 0, 0,
message, message_end - message);
if ((mac == NULL) ||
(memcmp(path, message, strlen(path)) != 0) ||
(memcmp(mac, message_end, maclen) != 0)) {
pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is "
"corrupted", path);
close(fd);
free(mac);
free(message);
return PAM_AUTH_ERR;
}
free(mac);
memmove(&then, message + strlen(path) + 1, sizeof(then));
free(message);
/* Check oldest login against timestamp */
if (get_ruser(pamh, ruser, sizeof(ruser)))
{
close(fd);
return PAM_AUTH_ERR;
}
if (check_login_time(ruser, then) != PAM_SUCCESS)
{
pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is "
"older than oldest login, disallowing "
"access to %s for user %s",
path, service, ruser);
close(fd);
return PAM_AUTH_ERR;
}
/* Compare the dates. */
now = time(NULL);
if (timestamp_good(then, now, interval) == PAM_SUCCESS) {
close(fd);
pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' is "
"only %ld seconds old, allowing access to %s "
"for user %s", path, (long) (now - st.st_mtime),
service, ruser);
if (verbose) {
verbose_success(pamh, now - st.st_mtime);
}
return PAM_SUCCESS;
} else {
close(fd);
pam_syslog(pamh, LOG_NOTICE, "timestamp file `%s' has "
"unacceptable age (%ld seconds), disallowing "
"access to %s for user %s",
path, (long) (now - st.st_mtime),
service, ruser);
return PAM_AUTH_ERR;
}
}
close(fd);
/* Fail by default. */
return PAM_AUTH_ERR;
}
int
pam_sm_setcred(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED, const char **argv UNUSED)
{
return PAM_SUCCESS;
}
int
pam_sm_open_session(pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv)
{
char path[BUFLEN], subdir[BUFLEN], *text, *p;
void *mac;
size_t maclen;
time_t now;
int fd, i, debug = 0;
/* Parse arguments. */
for (i = 0; i < argc; i++) {
if (strcmp(argv[i], "debug") == 0) {
debug = 1;
}
}
/* Get the name of the timestamp file. */
if (get_timestamp_name(pamh, argc, argv,
path, sizeof(path)) != PAM_SUCCESS) {
return PAM_SESSION_ERR;
}
/* Create the directory for the timestamp file if it doesn't already
* exist. */
for (i = 1; path[i] != '\0'; i++) {
if (path[i] == '/') {
/* Attempt to create the directory. */
strncpy(subdir, path, i);
subdir[i] = '\0';
if (mkdir(subdir, 0700) == 0) {
/* Attempt to set the owner to the superuser. */
if (lchown(subdir, 0, 0) != 0) {
if (debug) {
pam_syslog(pamh, LOG_DEBUG,
"error setting permissions on `%s': %m",
subdir);
}
return PAM_SESSION_ERR;
}
} else {
if (errno != EEXIST) {
if (debug) {
pam_syslog(pamh, LOG_DEBUG,
"error creating directory `%s': %m",
subdir);
}
return PAM_SESSION_ERR;
}
}
}
}
/* Generate the message. */
text = malloc(strlen(path) + 1 + sizeof(now) + hmac_sha1_size());
if (text == NULL) {
pam_syslog(pamh, LOG_CRIT, "unable to allocate memory: %m");
return PAM_SESSION_ERR;
}
p = text;
strcpy(text, path);
p += strlen(path) + 1;
now = time(NULL);
memmove(p, &now, sizeof(now));
p += sizeof(now);
/* Generate the MAC and append it to the plaintext. */
hmac_sha1_generate_file(pamh, &mac, &maclen,
TIMESTAMPKEY,
0, 0,
text, p - text);
if (mac == NULL) {
pam_syslog(pamh, LOG_ERR, "failure generating MAC: %m");
free(text);
return PAM_SESSION_ERR;
}
memmove(p, mac, maclen);
p += maclen;
free(mac);
/* Open the file. */
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
pam_syslog(pamh, LOG_ERR, "unable to open `%s': %m", path);
free(text);
return PAM_SESSION_ERR;
}
/* Attempt to set the owner to the superuser. */
if (fchown(fd, 0, 0) != 0) {
if (debug) {
pam_syslog(pamh, LOG_DEBUG,
"error setting ownership of `%s': %m",
path);
}
close(fd);
free(text);
return PAM_SESSION_ERR;
}
/* Write the timestamp to the file. */
if (write(fd, text, p - text) != p - text) {
pam_syslog(pamh, LOG_ERR, "unable to write to `%s': %m", path);
close(fd);
free(text);
return PAM_SESSION_ERR;
}
/* Close the file and return successfully. */
close(fd);
free(text);
pam_syslog(pamh, LOG_DEBUG, "updated timestamp file `%s'", path);
return PAM_SUCCESS;
}
int
pam_sm_close_session(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED, const char **argv UNUSED)
{
return PAM_SUCCESS;
}
#else /* PAM_TIMESTAMP_MAIN */
#define USAGE "Usage: %s [[-k] | [-d]] [target user]\n"
#define CHECK_INTERVAL 7
int
main(int argc, char **argv)
{
int i, retval = 0, dflag = 0, kflag = 0;
const char *target_user = NULL, *user = NULL, *tty = NULL;
struct passwd *pwd;
struct timeval tv;
fd_set write_fds;
char path[BUFLEN];
struct stat st;
/* Check that there's nothing funny going on with stdio. */
if ((fstat(STDIN_FILENO, &st) == -1) ||
(fstat(STDOUT_FILENO, &st) == -1) ||
(fstat(STDERR_FILENO, &st) == -1)) {
/* Appropriate the "no controlling tty" error code. */
return 3;
}
/* Parse arguments. */
while ((i = getopt(argc, argv, "dk")) != -1) {
switch (i) {
case 'd':
dflag++;
break;
case 'k':
kflag++;
break;
default:
fprintf(stderr, USAGE, argv[0]);
return 1;
break;
}
}
/* Bail if both -k and -d are given together. */
if ((kflag + dflag) > 1) {
fprintf(stderr, USAGE, argv[0]);
return 1;
}
/* Check that we're setuid. */
if (geteuid() != 0) {
fprintf(stderr, "%s must be setuid root\n",
argv[0]);
retval = 2;
}
/* Check that we have a controlling tty. */
tty = ttyname(STDIN_FILENO);
if ((tty == NULL) || (strlen(tty) == 0)) {
tty = ttyname(STDOUT_FILENO);
}
if ((tty == NULL) || (strlen(tty) == 0)) {
tty = ttyname(STDERR_FILENO);
}
if ((tty == NULL) || (strlen(tty) == 0)) {
tty = "unknown";
}
/* Get the name of the invoking (requesting) user. */
pwd = getpwuid(getuid());
if (pwd == NULL) {
retval = 4;
}
/* Get the name of the target user. */
user = strdup(pwd->pw_name);
if (user == NULL) {
retval = 4;
} else {
target_user = (optind < argc) ? argv[optind] : user;
if ((strchr(target_user, '.') != NULL) ||
(strchr(target_user, '/') != NULL) ||
(strchr(target_user, '%') != NULL)) {
fprintf(stderr, "unknown user: %s\n",
target_user);
retval = 4;
}
}
/* Sanity check the tty to make sure we should be checking
* for timestamps which pertain to it. */
if (retval == 0) {
tty = check_tty(tty);
if (tty == NULL) {
fprintf(stderr, "invalid tty\n");
retval = 6;
}
}
do {
/* Sanity check the timestamp directory itself. */
if (retval == 0) {
if (check_dir_perms(NULL, TIMESTAMPDIR) != PAM_SUCCESS) {
retval = 5;
}
}
if (retval == 0) {
/* Generate the name of the timestamp file. */
format_timestamp_name(path, sizeof(path), TIMESTAMPDIR,
tty, user, target_user);
}
if (retval == 0) {
if (kflag) {
/* Remove the timestamp. */
if (lstat(path, &st) != -1) {
retval = unlink(path);
}
} else {
/* Check the timestamp. */
if (lstat(path, &st) != -1) {
/* Check oldest login against timestamp */
if (check_login_time(user, st.st_mtime) != PAM_SUCCESS) {
retval = 7;
} else if (!timestamp_good(st.st_mtime, time(NULL),
DEFAULT_TIMESTAMP_TIMEOUT) == PAM_SUCCESS) {
retval = 7;
}
} else {
retval = 7;
}
}
}
if (dflag > 0) {
struct timeval now;
/* Send the would-be-returned value to our parent. */
signal(SIGPIPE, SIG_DFL);
fprintf(stdout, "%d\n", retval);
fflush(stdout);
/* Wait. */
gettimeofday(&now, NULL);
tv.tv_sec = CHECK_INTERVAL;
/* round the sleep time to get woken up on a whole second */
tv.tv_usec = 1000000 - now.tv_usec;
if (now.tv_usec < 500000)
tv.tv_sec--;
FD_ZERO(&write_fds);
FD_SET(STDOUT_FILENO, &write_fds);
select(STDOUT_FILENO + 1,
NULL, NULL, &write_fds,
&tv);
retval = 0;
}
} while (dflag > 0);
return retval;
}
#endif