From e428a379fc97db8400a497045a57a69468666901 Mon Sep 17 00:00:00 2001
From: TJ Saunders <tj@castaglia.org>
Date: Sat, 4 Sep 2021 10:19:01 -0700
Subject: [PATCH] Issue #1321: When processing very long lines from an
 `AuthGroupFile`, allocate larger buffers in smaller increments, as
 `fgetgrent(3)` does.

---
 modules/mod_auth_file.c                       | 11 ++-
 tests/t/lib/ProFTPD/TestSuite/Utils.pm        |  4 +-
 .../ProFTPD/Tests/Modules/mod_auth_file.pm    | 80 +++++++++++++++++++
 3 files changed, 92 insertions(+), 3 deletions(-)

--- proftpd.orig/modules/mod_auth_file.c
+++ proftpd/modules/mod_auth_file.c
@@ -332,6 +332,9 @@
 static char *af_getgrentline(char **buf, int *buflen, pr_fh_t *fh,
     unsigned int *lineno) {
   char *cp = *buf;
+  int original_buflen;
+
+  original_buflen = *buflen;
 
   while (pr_fsio_gets(cp, (*buflen) - (cp - *buf), fh) != NULL) {
     pr_signals_handle();
@@ -343,8 +346,12 @@
       return *buf;
     }
 
-    /* No -- allocate a larger buffer, doubling buflen. */
-    *buflen += *buflen;
+    /* No -- allocate a larger buffer.  Note that doubling the buflen
+     * each time may cause issues; fgetgrent(3) would increment the
+     * allocated buffer by the original buffer length each time.  So we
+     * do the same (Issue #1321).
+     */
+    *buflen += original_buflen;
 
     {
       char *new_buf;
--- proftpd.orig/tests/t/lib/ProFTPD/TestSuite/Utils.pm
+++ proftpd/tests/t/lib/ProFTPD/TestSuite/Utils.pm
@@ -1210,6 +1210,8 @@
   my $gid = shift;
   $gid = 500 unless defined($gid);
   my $home_dir = shift;
+  my $groups = shift;
+  $groups = $user unless defined($groups);
 
   my $config_file = "$tmpdir/$name.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/$name.pid");
@@ -1238,7 +1240,7 @@
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  auth_group_write($auth_group_file, $group, $gid, $groups);
 
   my $setup = {
     auth_user_file => $auth_user_file,
--- proftpd.orig/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_file.pm
+++ proftpd/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_file.pm
@@ -122,6 +122,10 @@
     test_class => [qw(bug forking)],
   },
 
+  auth_file_line_too_long_issue1321 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
 };
 
 sub new {
@@ -2271,4 +2275,80 @@
   unlink($log_file);
 }
 
+sub auth_file_line_too_long_issue1321 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  # For Issue #1321, we create a very long AuthGroupFile entry with many
+  # group names.
+
+  my $groups = 'proftpd';
+  for (my $i = 0; $i < 200; $i++) {
+    $groups .= ",quite.long.example.group.$i";
+  }
+
+  my $setup = test_setup($tmpdir, 'authfile', undef, undef, undef, undef, undef,
+    undef, $groups);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
 1;
