OpenBSD Security Advisory: BSD I/O Signals

Thomas H. Ptacek (tqbf@RDIST.ORG)
Mon, 15 Sep 1997 01:06:19 -0500

----------------------------------------------------------------------------

OpenBSD Security Advisory

September 15, 1997

Vulnerability in I/O Signal Handling

----------------------------------------------------------------------------

SYNOPSIS

A vulnerability discovered in the 4.4BSD kernel allows unprivileged users
to send certain signals to arbitrary processes on the system. Depending on
the operating system and targeted program, this may allow users to kill
off processes or disrupt the operation of certain programs.

----------------------------------------------------------------------------

AFFECTED SYSTEMS

This vulnerability has been tested on all available 4.4BSD-based operating
systems, including BSDI, NetBSD, OpenBSD, and FreeBSD, in their most
recent release revisions. Additionally, this problem is known to affect
SGI IRIX, and may affect other operating systems as well.

----------------------------------------------------------------------------

DETAILS

Certain programs implemented in Unix operating systems make use of a
facility called "asynchronous I/O" to handle multiple tasks
simultaneously. Asynchronous I/O allows a process to be notified whenever
it has data that needs to be read from an input source; the kernel
notifies the process using a signal.

Asynchronous I/O is enabled on a descriptor using the fcntl() system call;
a descriptor with the O_ASYNC flag set will cause a signal to be sent
whenever there is data available to be read from it. Additionally, using
the FIOASYNC ioctl(), asynchronous notification can be enabled on a
descriptor.

In cases where multiple processes are used in an application, it becomes
useful to allow a descriptor to notify other processes as well. This is
accomplished by use of another fcntl() operation, F_SETOWN, as well as an
ioctl, FIOSETOWN (certain devices also provide an interface to this
facility with the TIOCSPGRP ioctl). These operations allow a program to
set the process or process group that will receive signal notification of
pending I/O.

A lack of checking in the code that handles these operations allows a
program to specify an arbitrary process ID when using a socket or device
file descriptor. By setting the recipient of signal notification to a
process that is not owned by the program, the kernel can be tricked into
signalling arbitrary programs.

Additionally, vulnerable kernels do not keep track of the credentials
associated with a process when determining whether to send I/O signals;
because of this, it is possible to specify the PID of a process that is
owned by an attacker, and then destroy that process and wait for it's PID
to be re-used. The new process occupying that PID can then be signalled by
the attacker, regardless of it's owner.

It's important to note that operating systems that check credentials when
a program attempts to set the PID for I/O notification (thus evading part
of this vulnerability) may still be vulnerable to the latter problem
(process ID re-use), if credentials aren't checked at signal delivery
time. We recommend that concerned parties contact their operating system
vendors or support channels to verify their vulnerability status.

----------------------------------------------------------------------------

TECHNICAL DETAILS

This vulnerability exists due to a lack of credential checking in the
kernel when setting the recipient of I/O notification. BSD-based
kernels maintain this information in descriptor-specific data structures
unrelated to the process table; the vulnerability discussed here involves
sockets, which maintain the signal recipient in the per-descriptor
"socket" structure, although certain devices provide similar facilities
and vulnerabilities.

On 4.4BSD systems, the signal normally sent to inform a process of pending
I/O is SIGIO. The default disposition of the SIGIO signal is "ignore" -
thus, most processes are unaffected by this vulnerability. However,
certain programs explicitly catch SIGIO in order to use asynchronous I/O;
these programs can be disrupted by sending stray SIGIO's.

The process notification information is also used to determine the process
that receives notification of out-of-band data on a socket. The same
vulnerability applies, this time by setting the process to notify and then
sending a message with the MSG_OOB flag set; the targetted process will
receive a SIGURG signal. Certain network daemons (ftpd, for instance) can
be disrupted by being sent a stray SIGURG signal when no data is available
for reading.

The problem is more serious on vulnerable System V operating systems; in
many cases, SIGIO is the equivalent of SIGPOLL, and the default
disposition of that signal is "terminate process". On SGI's IRIX operating
system, exploitation of this vulnerability can kill any process on the
system.

In addition to being an extremely potent denial of service attack,
surgical application of this vulnerability can be used to compromise the
system - for example, a process holding a bound address (NFS port 2049,
for instance) can be killed off and it's port stolen; this can be used to
steal NFS file handles.

In addition to sockets, some devices also provide facilities for
notification of pending I/O; examples include the "log", "tun", and "bpf".
The BPF and tunnel devices are of minimal concern, as they are not
typically accessible by arbitrary users (although BPF is interesting in
that it will allow the owner of a bpf-associated descriptor to choose an
arbitrary signal to send, including SIGSTOP).

Unfortunately, the log device is normally accessible by users, and can be
used to perform the same attack as sockets allow. It's also worth noting
that the interface that allows programs to set the process to receive
notification of I/O on the log device renders the legitimate purpose of
this facility totally unreliable; unrelated programs can seize control of
the asynchronous I/O notification on the log device, causing programs that
rely on it to fail. The provided patches do not attempt to resolve this
problem.

----------------------------------------------------------------------------

RESOLUTION

This is a kernel problem that can only be fixed by patching the
problematic system code. Patches for the OpenBSD operating system are
provided in this advisory; FreeBSD is known to be working on a similar
resolution.

The provided OpenBSD patch causes the kernel to keep track of the
credentials of the process associated with an I/O object; the credentials
are checked whenever I/O notification will occur, and therefore resolve
both the F_SETOWN and PID-reuse problems. Device drivers that present an
interface to I/O notification must be modified to check credentials when
the TIOCSPGRP (or equivalent) ioctl() is used to set notificatio PID; the
OpenBSD patch resolves all currently known occurances of this in that
operating system.

----------------------------------------------------------------------------

CREDITS

This vulnerability is believed to have been discovered originally by Alan
Peakall. Documentation and testing of this problem was conducted by Theo
de Raadt and the OpenBSD development team; SGI information was obtained
from Timothy Newsham.

The OpenBSD patch for this vulnerability was written in a caffeinated haze
by Theo de Raadt of the OpenBSD project.

The developers at OpenBSD would like to thank Perry Metzger for his
continuous support of their work.

----------------------------------------------------------------------------

OPENBSD PATCH

Index: sys/signalvar.h
===================================================================
RCS file: /cvs/src/sys/sys/signalvar.h,v
retrieving revision 1.6
retrieving revision 1.7
diff -u -r1.6 -r1.7
--- signalvar.h 1997/02/01 21:49:36 1.6
+++ signalvar.h 1997/08/31 20:42:01 1.7
@@ -156,6 +156,7 @@
int coredump __P((struct proc *p));
void execsigs __P((struct proc *p));
void gsignal __P((int pgid, int sig));
+void csignal __P((pid_t pgid, int signum, uid_t uid, uid_t euid));
int issignal __P((struct proc *p));
void pgsignal __P((struct pgrp *pgrp, int sig, int checkctty));
void postsig __P((int sig));
Index: sys/socketvar.h
===================================================================
RCS file: /cvs/src/sys/sys/socketvar.h,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -r1.10 -r1.11
--- socketvar.h 1997/02/28 04:04:13 1.10
+++ socketvar.h 1997/08/31 20:42:02 1.11
@@ -71,6 +71,8 @@
short so_timeo; /* connection timeout */
u_short so_error; /* error affecting connection */
pid_t so_pgid; /* pgid for signals */
+ uid_t so_siguid; /* uid of process who set so_pgid */
+ uid_t so_sigeuid; /* euid of process who set so_pgid */
u_long so_oobmark; /* chars to oob mark */
/*
* Variables for socket buffering.
Index: kern/kern_descrip.c
===================================================================
RCS file: /cvs/src/sys/kern/kern_descrip.c,v
retrieving revision 1.13
retrieving revision 1.14
diff -u -r1.13 -r1.14
--- kern_descrip.c 1997/08/21 05:17:37 1.13
+++ kern_descrip.c 1997/08/31 20:42:15 1.14
@@ -55,6 +55,7 @@
#include <sys/fcntl.h>
#include <sys/malloc.h>
#include <sys/syslog.h>
+#include <sys/ucred.h>
#include <sys/unistd.h>
#include <sys/resourcevar.h>
#include <sys/conf.h>
@@ -251,8 +252,11 @@

case F_SETOWN:
if (fp->f_type == DTYPE_SOCKET) {
- ((struct socket *)fp->f_data)->so_pgid =
- (long)SCARG(uap, arg);
+ struct socket *so = (struct socket *)fp->f_data;
+
+ so->so_pgid = (long)SCARG(uap, arg);
+ so->so_siguid = p->p_cred->p_ruid;
+ so->so_sigeuid = p->p_ucred->cr_uid;
return (0);
}
if ((long)SCARG(uap, arg) <= 0) {
Index: kern/kern_sig.c
===================================================================
RCS file: /cvs/src/sys/kern/kern_sig.c,v
retrieving revision 1.16
retrieving revision 1.17
diff -u -r1.16 -r1.17
--- kern_sig.c 1997/02/01 21:49:41 1.16
+++ kern_sig.c 1997/08/31 20:42:18 1.17
@@ -481,6 +481,46 @@
}
}
return (nfound ? 0 : ESRCH);
+}
+
+#define CANDELIVER(uid, euid, p) \
+ (euid == 0 || \
+ (uid) == (p)->p_cred->p_ruid || \
+ (uid) == (p)->p_cred->p_svuid || \
+ (uid) == (p)->p_ucred->cr_uid || \
+ (euid) == (p)->p_cred->p_ruid || \
+ (euid) == (p)->p_cred->p_svuid || \
+ (euid) == (p)->p_ucred->cr_uid)
+
+/*
+ * Deliver signum to pgid, but first check uid/euid against each
+ * process and see if it is permitted.
+ */
+void
+csignal(pgid, signum, uid, euid)
+ pid_t pgid;
+ int signum;
+ uid_t uid, euid;
+{
+ struct pgrp *pgrp;
+ struct proc *p;
+
+ if (pgid == 0)
+ return;
+ if (pgid < 0) {
+ pgid = -pgid;
+ if ((pgrp = pgfind(pgid)) == NULL)
+ return;
+ for (p = pgrp->pg_members.lh_first; p;
+ p = p->p_pglist.le_next)
+ if (CANDELIVER(uid, euid, p))
+ psignal(p, signum);
+ } else {
+ if ((p = pfind(pgid)) == NULL)
+ return;
+ if (CANDELIVER(uid, euid, p))
+ psignal(p, signum);
+ }
}

/*
Index: kern/subr_log.c
===================================================================
RCS file: /cvs/src/sys/kern/subr_log.c,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -r1.3 -r1.4
--- subr_log.c 1996/04/21 22:27:17 1.3
+++ subr_log.c 1997/08/31 20:42:20 1.4
@@ -60,6 +60,8 @@
int sc_state; /* see above for possibilities */
struct selinfo sc_selp; /* process waiting on select call */
int sc_pgid; /* process/group for async I/O */
+ uid_t sc_siguid; /* uid for process that set sc_pgid */
+ uid_t sc_sigeuid; /* euid for process that set sc_pgid */
} logsoftc;

int log_open; /* also used in log() */
@@ -179,17 +181,12 @@
void
logwakeup()
{
- struct proc *p;
-
if (!log_open)
return;
selwakeup(&logsoftc.sc_selp);
- if (logsoftc.sc_state & LOG_ASYNC) {
- if (logsoftc.sc_pgid < 0)
- gsignal(-logsoftc.sc_pgid, SIGIO);
- else if ((p = pfind(logsoftc.sc_pgid)) != NULL)
- psignal(p, SIGIO);
- }
+ if (logsoftc.sc_state & LOG_ASYNC)
+ csignal(logsoftc.sc_pgid, SIGIO,
+ logsoftc.sc_siguid, logsoftc.sc_sigeuid);
if (logsoftc.sc_state & LOG_RDWAIT) {
wakeup((caddr_t)msgbufp);
logsoftc.sc_state &= ~LOG_RDWAIT;
@@ -232,6 +229,8 @@

case TIOCSPGRP:
logsoftc.sc_pgid = *(int *)data;
+ logsoftc.sc_siguid = p->p_cred->p_ruid;
+ logsoftc.sc_sigeuid = p->p_ucred->cr_uid;
break;

case TIOCGPGRP:
Index: kern/sys_generic.c
===================================================================
RCS file: /cvs/src/sys/kern/sys_generic.c,v
retrieving revision 1.7
retrieving revision 1.8
diff -u -r1.7 -r1.8
--- sys_generic.c 1997/01/27 23:21:13 1.7
+++ sys_generic.c 1997/08/31 20:42:21 1.8
@@ -480,7 +480,11 @@
case FIOSETOWN:
tmp = *(int *)data;
if (fp->f_type == DTYPE_SOCKET) {
- ((struct socket *)fp->f_data)->so_pgid = tmp;
+ struct socket *so = (struct socket *)fp->f_data;
+
+ so->so_pgid = tmp;
+ so->so_siguid = p->p_cred->p_ruid;
+ so->so_sigeuid = p->p_ucred->cr_uid;
error = 0;
break;
}
Index: kern/sys_socket.c
===================================================================
RCS file: /cvs/src/sys/kern/sys_socket.c,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- sys_socket.c 1997/02/24 14:19:59 1.2
+++ sys_socket.c 1997/08/31 20:42:23 1.3
@@ -39,6 +39,7 @@
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/file.h>
+#include <sys/proc.h>
#include <sys/mbuf.h>
#include <sys/protosw.h>
#include <sys/socket.h>
@@ -112,6 +113,8 @@

case SIOCSPGRP:
so->so_pgid = *(int *)data;
+ so->so_siguid = p->p_cred->p_ruid;
+ so->so_sigeuid = p->p_ucred->cr_uid;
return (0);

case SIOCGPGRP:
Index: kern/uipc_socket.c
===================================================================
RCS file: /cvs/src/sys/kern/uipc_socket.c,v
retrieving revision 1.15
retrieving revision 1.17
diff -u -r1.15 -r1.17
--- uipc_socket.c 1997/06/29 18:14:35 1.15
+++ uipc_socket.c 1997/08/31 20:42:24 1.17
@@ -1058,11 +1060,6 @@
sohasoutofband(so)
register struct socket *so;
{
- struct proc *p;
-
- if (so->so_pgid < 0)
- gsignal(-so->so_pgid, SIGURG);
- else if (so->so_pgid > 0 && (p = pfind(so->so_pgid)) != 0)
- psignal(p, SIGURG);
+ csignal(so->so_pgid, SIGURG, so->so_siguid, so->so_sigeuid);
selwakeup(&so->so_rcv.sb_sel);
}
Index: kern/uipc_socket2.c
===================================================================
RCS file: /cvs/src/sys/kern/uipc_socket2.c,v
retrieving revision 1.5
retrieving revision 1.6
diff -u -r1.5 -r1.6
--- uipc_socket2.c 1997/02/21 08:45:00 1.5
+++ uipc_socket2.c 1997/08/31 20:42:26 1.6
@@ -315,20 +315,14 @@
register struct socket *so;
register struct sockbuf *sb;
{
- struct proc *p;
-
selwakeup(&sb->sb_sel);
sb->sb_flags &= ~SB_SEL;
if (sb->sb_flags & SB_WAIT) {
sb->sb_flags &= ~SB_WAIT;
wakeup((caddr_t)&sb->sb_cc);
}
- if (so->so_state & SS_ASYNC) {
- if (so->so_pgid < 0)
- gsignal(-so->so_pgid, SIGIO);
- else if (so->so_pgid > 0 && (p = pfind(so->so_pgid)) != 0)
- psignal(p, SIGIO);
- }
+ if (so->so_state & SS_ASYNC)
+ csignal(so->so_pgid, SIGIO, so->so_siguid, so->so_sigeuid);
}

/*
Index: net/bpf.c
===================================================================
RCS file: /cvs/src/sys/net/bpf.c,v
retrieving revision 1.9
retrieving revision 1.10
diff -u -r1.9 -r1.10
--- bpf.c 1997/03/17 16:29:37 1.9
+++ bpf.c 1997/08/31 20:42:29 1.10
@@ -522,14 +522,10 @@
bpf_wakeup(d)
register struct bpf_d *d;
{
- struct proc *p;
-
wakeup((caddr_t)d);
if (d->bd_async && d->bd_sig)
- if (d->bd_pgid > 0)
- gsignal (d->bd_pgid, d->bd_sig);
- else if ((p = pfind (-d->bd_pgid)) != NULL)
- psignal (p, d->bd_sig);
+ csignal(d->bd_pgid, d->bd_sig,
+ d->bd_siguid, d->bd_sigeuid);

#if BSD >= 199103
selwakeup(&d->bd_sel);
@@ -822,6 +818,8 @@
*/
case TIOCSPGRP: /* Process or group to send signals to */
d->bd_pgid = *(int *)addr;
+ d->bd_siguid = p->p_cred->p_ruid;
+ d->bd_sigeuid = p->p_ucred->cr_uid;
break;

case TIOCGPGRP:
Index: net/bpfdesc.h
===================================================================
RCS file: /cvs/src/sys/net/bpfdesc.h,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- bpfdesc.h 1997/02/24 13:33:56 1.2
+++ bpfdesc.h 1997/08/31 20:42:30 1.3
@@ -77,6 +77,8 @@
int bd_async; /* non-zero if packet reception should generate signal */
int bd_sig; /* signal to send upon packet reception */
pid_t bd_pgid; /* process or group id for signal */
+ uid_t bd_siguid; /* uid for process that set pgid */
+ uid_t bd_sigeuid; /* euid for process that set pgid */
#if BSD < 199103
u_char bd_selcoll; /* true if selects collide */
int bd_timedout;
Index: net/if_tun.c
===================================================================
RCS file: /cvs/src/sys/net/if_tun.c,v
retrieving revision 1.19
retrieving revision 1.20
diff -u -r1.19 -r1.20
--- if_tun.c 1997/07/29 07:18:20 1.19
+++ if_tun.c 1997/08/31 20:42:32 1.20
@@ -84,7 +84,9 @@
struct tun_softc {
u_short tun_flags; /* misc flags */
struct ifnet tun_if; /* the interface */
- int tun_pgrp; /* the process group - if any */
+ pid_t tun_pgid; /* the process group - if any */
+ uid_t tun_siguid; /* uid for process that set tun_pgid */
+ uid_t tun_sigeuid; /* euid for process that set tun_pgid */
struct selinfo tun_rsel; /* read select */
struct selinfo tun_wsel; /* write select (not used) */
};
@@ -228,7 +230,7 @@
}
splx(s);
}
- tp->tun_pgrp = 0;
+ tp->tun_pgid = 0;
selwakeup(&tp->tun_rsel);

TUNDEBUG(("%s: closed\n", ifp->if_xname));
@@ -331,7 +333,6 @@
{
struct tun_softc *tp = ifp->if_softc;
struct tunnel_header *th;
- struct proc *p;
int s;

TUNDEBUG(("%s: tun_output\n", ifp->if_xname));
@@ -371,12 +372,9 @@
tp->tun_flags &= ~TUN_RWAIT;
wakeup((caddr_t)tp);
}
- if (tp->tun_flags & TUN_ASYNC && tp->tun_pgrp) {
- if (tp->tun_pgrp > 0)
- gsignal(tp->tun_pgrp, SIGIO);
- else if ((p = pfind(-tp->tun_pgrp)) != NULL)
- psignal(p, SIGIO);
- }
+ if (tp->tun_flags & TUN_ASYNC && tp->tun_pgid)
+ csignal(tp->tun_pgid, SIGIO,
+ tp->tun_siguid, tp->tun_sigeuid);
selwakeup(&tp->tun_rsel);
return 0;
}
@@ -446,10 +444,12 @@
splx(s);
break;
case TIOCSPGRP:
- tp->tun_pgrp = *(int *)data;
+ tp->tun_pgid = *(int *)data;
+ tp->tun_siguid = p->p_cred->p_ruid;
+ tp->tun_sigeuid = p->p_ucred->cr_uid;
break;
case TIOCGPGRP:
- *(int *)data = tp->tun_pgrp;
+ *(int *)data = tp->tun_pgid;
break;
default:
splx(s);