tty: n_hdlc: make n_hdlc_tty_wakeup() asynchronous
authorTetsuo Handa <penguin-kernel@i-love.sakura.ne.jp>
Wed, 15 Dec 2021 11:52:40 +0000 (20:52 +0900)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Wed, 22 Dec 2021 08:32:45 +0000 (09:32 +0100)
commit 1ee33b1ca2b8dabfcc17198ffd049a6b55674a86 upstream.

syzbot is reporting that an unprivileged user who logged in from tty
console can crash the system using a reproducer shown below [1], for
n_hdlc_tty_wakeup() is synchronously calling n_hdlc_send_frames().

----------
  #include <sys/ioctl.h>
  #include <unistd.h>

  int main(int argc, char *argv[])
  {
    const int disc = 0xd;

    ioctl(1, TIOCSETD, &disc);
    while (1) {
      ioctl(1, TCXONC, 0);
      write(1, "", 1);
      ioctl(1, TCXONC, 1); /* Kernel panic - not syncing: scheduling while atomic */
    }
  }
----------

Linus suspected that "struct tty_ldisc"->ops->write_wakeup() must not
sleep, and Jiri confirmed it from include/linux/tty_ldisc.h. Thus, defer
n_hdlc_send_frames() from n_hdlc_tty_wakeup() to a WQ context like
net/nfc/nci/uart.c does.

Link: https://syzkaller.appspot.com/bug?extid=5f47a8cea6a12b77a876
Reported-by: syzbot <syzbot+5f47a8cea6a12b77a876@syzkaller.appspotmail.com>
Cc: stable <stable@vger.kernel.org>
Analyzed-by: Fabio M. De Francesco <fmdefrancesco@gmail.com>
Suggested-by: Linus Torvalds <torvalds@linux-foundation.org>
Confirmed-by: Jiri Slaby <jirislaby@kernel.org>
Reviewed-by: Fabio M. De Francesco <fmdefrancesco@gmail.com>
Signed-off-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>
Link: https://lore.kernel.org/r/40de8b7e-a3be-4486-4e33-1b1d1da452f8@i-love.sakura.ne.jp
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/tty/n_hdlc.c

index 580a37b3fe1b9bc7d3dfce0d656e49aec5e58d29..166f651fde33a29991645c0e4402e5ea28813b44 100644 (file)
@@ -140,6 +140,8 @@ struct n_hdlc {
        struct n_hdlc_buf_list  rx_buf_list;
        struct n_hdlc_buf_list  tx_free_buf_list;
        struct n_hdlc_buf_list  rx_free_buf_list;
+       struct work_struct      write_work;
+       struct tty_struct       *tty_for_write_work;
 };
 
 /*
@@ -154,6 +156,7 @@ static struct n_hdlc_buf *n_hdlc_buf_get(struct n_hdlc_buf_list *list);
 /* Local functions */
 
 static struct n_hdlc *n_hdlc_alloc(void);
+static void n_hdlc_tty_write_work(struct work_struct *work);
 
 /* max frame size for memory allocations */
 static int maxframe = 4096;
@@ -210,6 +213,8 @@ static void n_hdlc_tty_close(struct tty_struct *tty)
        wake_up_interruptible(&tty->read_wait);
        wake_up_interruptible(&tty->write_wait);
 
+       cancel_work_sync(&n_hdlc->write_work);
+
        n_hdlc_free_buf_list(&n_hdlc->rx_free_buf_list);
        n_hdlc_free_buf_list(&n_hdlc->tx_free_buf_list);
        n_hdlc_free_buf_list(&n_hdlc->rx_buf_list);
@@ -241,6 +246,8 @@ static int n_hdlc_tty_open(struct tty_struct *tty)
                return -ENFILE;
        }
 
+       INIT_WORK(&n_hdlc->write_work, n_hdlc_tty_write_work);
+       n_hdlc->tty_for_write_work = tty;
        tty->disc_data = n_hdlc;
        tty->receive_room = 65536;
 
@@ -334,6 +341,20 @@ check_again:
                goto check_again;
 }      /* end of n_hdlc_send_frames() */
 
+/**
+ * n_hdlc_tty_write_work - Asynchronous callback for transmit wakeup
+ * @work: pointer to work_struct
+ *
+ * Called when low level device driver can accept more send data.
+ */
+static void n_hdlc_tty_write_work(struct work_struct *work)
+{
+       struct n_hdlc *n_hdlc = container_of(work, struct n_hdlc, write_work);
+       struct tty_struct *tty = n_hdlc->tty_for_write_work;
+
+       n_hdlc_send_frames(n_hdlc, tty);
+}      /* end of n_hdlc_tty_write_work() */
+
 /**
  * n_hdlc_tty_wakeup - Callback for transmit wakeup
  * @tty: pointer to associated tty instance data
@@ -344,7 +365,7 @@ static void n_hdlc_tty_wakeup(struct tty_struct *tty)
 {
        struct n_hdlc *n_hdlc = tty->disc_data;
 
-       n_hdlc_send_frames(n_hdlc, tty);
+       schedule_work(&n_hdlc->write_work);
 }      /* end of n_hdlc_tty_wakeup() */
 
 /**