tech-userlevel archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

Proposal for write(1) addition



Hi,

write(1) is a nice tool to communicate safely on multi user machines. I've
been using it a while but there is one great insufficiency: It does not work
together with non-utmp terminals, as e.g. tmux(1) or rather any tool except
for screen creates as a normal user.

The patch supplied fixes that: When using the option '-c', write additionally
checks for a mounted ptyfs and loops through these terminals, comparing
ownership. As they only exist if there's a user logged into them, that works.

Now, you can use the 'newest' terminal of the user if he is logged in. If he
is not (i.e. has _no_ utmp-record), then you cannot write to him without
explicitly specifying a terminal.

Another issue fixed is that until now, the message 'not logged in' could
never have appeared because in case you could not open the terminal, it was
an EPERM instead of comparing ownership.

Note that you can only test this with a very current version as of after
http://mail-index.netbsd.org/source-changes/2011/09/16/msg027294.html.
If you want to test it anyway, comment out lines 207-212 and 242-243 in
write.c, then it works as intended.
This is also why I write to this list now: I won't have the chance to test
this patch any further the next few weeks as I have no machine I can reboot.
I can only test with these lines commented out.

I would be glad if somebody reviewed this code and perhaps eventually commits
it if it is usable. If not, then please comment what's wrong or recode this
functionality, nowadays write(1) is barely usable without.
One could also think about not using an option for that but making it the
standard behaviour, I was just curious about changing behaviour that is
longer present than I am.


Regards, Julian
Index: usr.bin/write/term_chk.c
===================================================================
RCS file: /cvsroot/src/usr.bin/write/term_chk.c,v
retrieving revision 1.8
diff -u -r1.8 term_chk.c
--- usr.bin/write/term_chk.c    14 Apr 2009 07:59:17 -0000      1.8
+++ usr.bin/write/term_chk.c    16 Sep 2011 22:56:53 -0000
@@ -47,6 +47,7 @@
 #include <paths.h>
 #include <fcntl.h>
 #include <string.h>
+#include <stdlib.h>
 #include <err.h>
 
 #include "term_chk.h"
@@ -59,21 +60,36 @@
 term_chk(uid_t uid, const char *tty, int *msgsokP, time_t *atimeP, int ismytty,
     gid_t saved_egid)
 {
-       char path[MAXPATHLEN];
+       char *path;
        struct stat s;
        int i, fd, serrno;
 
-       if (strstr(tty, "../") != NULL) {
-               errno = EINVAL;
-               return -1;
+       /* We can either get ttys of the form 'pts/4' (as getutentries outputs)
+        * or as absolute paths. In the former, automatically prepend /dev. */
+       if (strstr(tty, "/") == tty || strstr(tty, ".") == tty) {
+               path = strndup(tty, MAXPATHLEN);
+       } else {
+               path = malloc(sizeof(char)*MAXPATHLEN);
+               i = snprintf(path, MAXPATHLEN-1, _PATH_DEV "%s", tty);
+               if (i < 0 || i >= MAXPATHLEN) {
+                       errno = ENOMEM;
+                       return -1;
+               }
        }
-       i = snprintf(path, sizeof path, _PATH_DEV "%s", tty);
-       if (i < 0 || i >= (int)sizeof(path)) {
-               errno = ENOMEM;
+
+       (void)setegid(saved_egid);
+
+       if (stat(path, &s) == -1)
+               return -1;
+       if (msgsokP) /* group write bit */
+               *msgsokP = (s.st_mode & S_IWGRP);
+       if (atimeP)
+               *atimeP = s.st_atime;
+       if (s.st_uid != uid) {
+               errno = EPERM;
                return -1;
        }
 
-       (void)setegid(saved_egid);
        fd = open(path, O_WRONLY, 0);
        serrno = errno;
        (void)setegid(getgid());
@@ -81,21 +97,12 @@
 
        if (fd == -1)
                return(-1);
-       if (fstat(fd, &s) == -1)
-               goto error;
        if (!isatty(fd))
                goto error;
-       if (s.st_uid != uid && uid != 0) {
-               errno = EPERM;
-               goto error;
-       }
-       if (msgsokP)
-               *msgsokP = (s.st_mode & S_IWGRP) != 0;  /* group write bit */
-       if (atimeP)
-               *atimeP = s.st_atime;
        if (ismytty)
                (void)close(fd);
        return ismytty ? 0 : fd;
+
 error:
        if (fd != -1) {
                serrno = errno;
Index: usr.bin/write/write.c
===================================================================
RCS file: /cvsroot/src/usr.bin/write/write.c,v
retrieving revision 1.27
diff -u -r1.27 write.c
--- usr.bin/write/write.c       6 Sep 2011 18:46:35 -0000       1.27
+++ usr.bin/write/write.c       16 Sep 2011 22:54:02 -0000
@@ -1,4 +1,4 @@
-/*     $NetBSD: write.c,v 1.27 2011/09/06 18:46:35 joerg Exp $ */
+/*     $NetBSD: write.c,v 1.25 2008/07/21 14:19:28 lukem Exp $ */
 
 /*
  * Copyright (c) 1989, 1993
@@ -42,42 +42,53 @@
 #if 0
 static char sccsid[] = "@(#)write.c    8.2 (Berkeley) 4/27/95";
 #else
-__RCSID("$NetBSD: write.c,v 1.27 2011/09/06 18:46:35 joerg Exp $");
+__RCSID("$NetBSD: write.c,v 1.25 2008/07/21 14:19:28 lukem Exp $");
 #endif
 #endif /* not lint */
 
-#include <sys/types.h>
+#include <sys/mount.h>
 #include <sys/param.h>
 #include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <sys/types.h>
 #include <ctype.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <paths.h>
+#include <pwd.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <signal.h>
 #include <time.h>
-#include <fcntl.h>
-#include <paths.h>
-#include <pwd.h>
 #include <unistd.h>
-#include <err.h>
-#include <errno.h>
 
 #include "utmpentry.h"
 #include "term_chk.h"
 
-__dead static void done(int);
-static void do_write(int, const char *, const uid_t);
-static void wr_fputs(char *);
-static int search_utmp(char *, char *, uid_t, gid_t);
-static int utmp_chk(const char *, const char *);
+void usage(void);
+void done(int);
+void do_write(int, const char *);
+void wr_fputs(char *);
+int search_ptys(char *, char *, gid_t, int);
+int utmp_chk(const char *, const char *);
+int main(int, char **);
+int do_term_chk(const char*, time_t*, gid_t, int, char*, int*, char*);
+
+static int nttys;
+static uid_t myuid;
+static uid_t fuid;
 
 int
 main(int argc, char **argv)
 {
        time_t atime;
-       uid_t myuid, uid;
-       int msgsok, ttyfd;
+       int msgsok, ttyfd, searchptys = 0;
        char *mytty;
+       char ch;
        gid_t saved_egid = getegid();
 
        if (setegid(getgid()) == -1)
@@ -88,32 +99,42 @@
        mytty = check_sender(&atime, myuid, saved_egid);
 
        /* check args */
+       while ((ch = getopt(argc, argv, "c")) != -1) {
+               if (ch == 'c')
+                       searchptys = 1;
+               else
+                       usage();
+       }
+       argc -= optind;
+       argv += optind;
+
+       if (uid_from_user(argv[0], &fuid) == -1)
+               errx(1, "%s: unknown user", argv[0]);
+
        switch (argc) {
-       case 2:
-               ttyfd = search_utmp(argv[1], mytty, myuid, saved_egid);
+       case 1: /* Only user specified */
+               ttyfd = search_ptys(argv[0], mytty, saved_egid, searchptys);
                break;
-       case 3:
-               if (!strncmp(argv[2], _PATH_DEV, strlen(_PATH_DEV)))
-                       argv[2] += strlen(_PATH_DEV);
-               if (uid_from_user(argv[1], &uid) == -1)
-                       errx(1, "%s: unknown user", argv[1]);
-               if (utmp_chk(argv[1], argv[2]))
+
+       case 2: /* User and tty specified */
+               ttyfd = term_chk(fuid, argv[1], &msgsok, &atime, 0, saved_egid);
+               if (errno == EPERM)
                        errx(1, "%s is not logged in on %s",
-                           argv[1], argv[2]);
-               ttyfd = term_chk(uid, argv[2], &msgsok, &atime, 0, saved_egid);
-               if (ttyfd == -1)
-                       err(1, "%s%s", _PATH_DEV, argv[2]);
+                       argv[0], argv[1]);
                if (myuid && !msgsok)
                        errx(1, "%s has messages disabled on %s",
-                           argv[1], argv[2]);
+                                               argv[0], argv[1]);
+               if (ttyfd == -1)
+                       err(1, "%s", argv[1]);
                break;
+
        default:
-               (void)fprintf(stderr, "usage: write user [tty]\n");
-               exit(1);
+               usage();
        }
+
        if (setgid(getgid()) == -1)
                err(1, "setgid");
-       do_write(ttyfd, mytty, myuid);
+       do_write(ttyfd, mytty);
        done(0);
        /* NOTREACHED */
 #ifdef __GNUC__
@@ -121,11 +142,18 @@
 #endif
 }
 
+void
+usage(void)
+{
+       (void)fprintf(stderr, "usage: write [-c] user [tty]\n");
+       exit(1);
+}
+
 /*
  * utmp_chk - checks that the given user is actually logged in on
  *     the given tty
  */
-static int
+int
 utmp_chk(const char *user, const char *tty)
 {
        struct utmpentry *ep;
@@ -139,7 +167,8 @@
 }
 
 /*
- * search_utmp - search utmp for the "best" terminal to write to
+ * search_ptys - search utmp for the "best" terminal to write to, and, if
+ * searchptys is specified, search also for ptys in a mounted ptyfs.
  *
  * Ignores terminals with messages disabled, and of the rest, returns
  * the one with the most recent access time.  Returns as value the number
@@ -148,19 +177,24 @@
  *
  * Special case for writing to yourself - ignore the terminal you're
  * writing from, unless that's the only terminal with messages enabled.
+ *
+ * After looping through utmp for users logged in, we check for ptys and ttys 
in
+ * mounted ptyfs for terminals not marked in utmp. This has become necessary as
+ * nowadays there are user tools that open up new terminals (e.g. tmux), but
+ * don't have permissions to utmp.
  */
-static int
-search_utmp(char *user, char *mytty, uid_t myuid, gid_t saved_egid)
+int
+search_ptys(char *user, char *mytty, gid_t saved_egid, int searchptys)
 {
-       char tty[MAXPATHLEN];
-       time_t bestatime, atime;
-       int nloggedttys, nttys, msgsok, user_is_me;
+       char tty[MAXPATHLEN]; // This value is hardoded in do_term_chk, too.
+       time_t bestatime;
+       int nloggedttys, user_is_me, mntcnt;
        struct utmpentry *ep;
-       int fd, nfd;
-       uid_t uid;
-
-       if (uid_from_user(user, &uid) == -1)
-               errx(1, "%s: unknown user", user);
+       struct dirent *dp;
+       struct statvfs *mntpnts;
+       int fd, i;
+       char *tmppath;
+       DIR *dir;
 
        (void)getutentries(NULL, &ep);
 
@@ -168,34 +202,42 @@
        bestatime = 0;
        user_is_me = 0;
        fd = -1;
+
+       /* Check utmp records. */
        for (; ep; ep = ep->next)
                if (strcmp(user, ep->name) == 0) {
                        ++nloggedttys;
-                       nfd = term_chk(uid, ep->line, &msgsok, &atime, 0,
-                           saved_egid);
-                       if (nfd == -1)
-                               continue;       /* bad term? skip */
-                       if (myuid && !msgsok) {
-                               close(nfd);
-                               continue;       /* skip ttys with msgs off */
-                       }
-                       if (strcmp(ep->line, mytty) == 0) {
-                               user_is_me = 1;
-                               if (fd == -1)
-                                       fd = nfd;
-                               else
-                                       close(nfd);
-                               continue;       /* don't write to yourself */
+                       fd = do_term_chk(ep->line, &bestatime, saved_egid,
+                                               fd, mytty, &user_is_me, tty);
+               }
+
+       /* We would like to check /dev as well, but we can't. Opening unused
+        * terminals results in the open call blocking, so we only take the ones
+        * created by ptyfs. They're guaranteed to exist. */
+
+       /* Check ptyfs records */
+       if (searchptys) {
+               mntcnt = getmntinfo(&mntpnts, ST_NOWAIT);
+               for (i = 0; i < mntcnt; i++) { /* Silently ignore errors. */
+                       mntpnts++;
+                       if ((strcmp(mntpnts->f_fstypename, MOUNT_PTYFS) != 0)
+                       || ((dir = opendir(mntpnts->f_mntonname)) == NULL))
+                               continue;
+
+                       while ((dp = readdir(dir)) != NULL) {
+                               if ((asprintf(&tmppath, "%s/%s",
+                                               mntpnts->f_mntonname,
+                                               dp->d_name)) == 0)
+                                       continue;
+                               fd = do_term_chk(tmppath, &bestatime,
+                                               saved_egid, fd, mytty,
+                                               &user_is_me, tty);
+                               free(tmppath);
                        }
-                       ++nttys;
-                       if (atime > bestatime) {
-                               bestatime = atime;
-                               (void)strlcpy(tty, ep->line, sizeof(tty));
-                               close(fd);
-                               fd = nfd;
-                       } else
-                               close(nfd);
+                       closedir(dir);
+                       break;
                }
+       }
 
        if (nloggedttys == 0)
                errx(1, "%s is not logged in", user);
@@ -206,14 +248,64 @@
        } else if (nttys > 1)
                warnx("%s is logged in more than once; writing to %s",
                    user, tty);
+
        return fd;
 }
 
 /*
+ * do_term_chk - Just a wrapper around term_chk.
+ *
+ * Gets additionally fd, the foregoing "best" terminal and returns the fp of 
the
+ * new one, closing the old one if appropriate.
+ */
+int
+do_term_chk(const char *opentty, time_t *bestatime,
+                       gid_t saved_egid, int ofd,
+                       char *mytty, int *user_is_meP, char *tty)
+{
+       int nfd, msgsok;
+       time_t atime;
+
+       nfd = term_chk(fuid, opentty, &msgsok, &atime, 0, saved_egid);
+       if (nfd == -1)
+               return ofd;     /* bad term? skip */
+       if (myuid && !msgsok) {
+               close(nfd);
+               return ofd;     /* skip ttys with msgs off */
+       }
+
+       if (!strcmp(opentty, mytty)) {
+               *user_is_meP = 1;
+               if (ofd == -1)
+                       ofd = nfd;
+               else
+                       close(nfd);
+               return ofd;     /* don't write to yourself */
+       }
+
+       /* At this place, we take greater or equal instead of only greater than.
+        * The reason is: Cascaded terminals are created one after the other. We
+        * want the message to go to the highest-possible terminal to not send
+        * it to a container (e.g. tmux or a terminal emulation). So, we simply
+        * take the newest one (by creation). */
+       nttys++;
+       if (atime >= *bestatime) {
+               *bestatime = atime;
+               (void)strlcpy(tty, opentty, MAXPATHLEN);
+               if (ofd != -1)
+                       close(ofd);
+               return nfd;
+       } else {
+               close(nfd);
+       }
+       return ofd;
+}
+
+/*
  * do_write - actually make the connection
  */
-static void
-do_write(int ttyfd, const char *mytty, const uid_t myuid)
+void
+do_write(int ttyfd, const char *mytty)
 {
        const char *login;
        char *nows;
@@ -240,7 +332,7 @@
                (void)strlcpy(host, "???", sizeof(host));
        else
                host[sizeof(host) - 1] = '\0';
-       now = time(NULL);
+       now = time((time_t *)NULL);
        nows = ctime(&now);
        nows[16] = '\0';
        (void)printf("\r\n\a\a\aMessage from %s@%s on %s at %s ...\r\n",
@@ -253,7 +345,7 @@
 /*
  * done - cleanup and exit
  */
-static void
+void
 done(int signo)
 {
 
@@ -268,7 +360,7 @@
  * wr_fputs - like fputs(), but makes control characters visible and
  *     turns \n into \r\n
  */
-static void
+void
 wr_fputs(char *s)
 {
        unsigned char c;
Index: usr.bin/write/write.1
===================================================================
RCS file: /cvsroot/src/usr.bin/write/write.1,v
retrieving revision 1.6
diff -u -r1.6 write.1
--- usr.bin/write/write.1       7 Aug 2003 11:17:48 -0000       1.6
+++ usr.bin/write/write.1       16 Sep 2011 23:29:18 -0000
@@ -40,6 +40,7 @@
 .Nd send a message to another user
 .Sh SYNOPSIS
 .Nm
+.Op Fl c
 .Ar user
 .Op Ar ttyname
 .Sh DESCRIPTION
@@ -86,6 +87,12 @@
 idle time.
 This is so that if the user is logged in at work and also dialed up from
 home, the message will go to the right place.
+When using terminal multiplexers that do not update
+.Xr utmp 5
+records, you can use the option
+.Fl c
+to check not only terminals where the user started a login shell, but all in
+a mounted ptyfs (if any) that belong to the user.
 .Pp
 The traditional protocol for writing to someone is that the string
 .Ql \-o ,
@@ -95,6 +102,12 @@
 .Ql oo
 means that the person believes the conversation to be
 over.
+.Sh OPTIONS
+.Bl -tag
+.It Fl c
+Activate writing not only to
+.Xr utmp 5
+recorded terminals, but all terminals the user currently uses.
 .Sh SEE ALSO
 .Xr mesg 1 ,
 .Xr talk 1 ,

Attachment: signature.asc
Description: PGP signature



Home | Main Index | Thread Index | Old Index