[Git][debian-proftpd-team/proftpd-mod-procfs][master] 3 commits: New upstream version 0.3

Hilmar Preuße (@hilmar) gitlab at salsa.debian.org
Sat Aug 1 19:29:40 BST 2026



Hilmar Preuße pushed to branch master at Debian ProFTPD Team / proftpd-mod-procfs


Commits:
b43831b5 by Hilmar Preuße at 2026-08-01T09:15:18+02:00
New upstream version 0.3
- - - - -
b2c75f4d by Hilmar Preuße at 2026-08-01T09:15:18+02:00
Update upstream source from tag 'upstream/0.3'

Update to upstream version '0.3'
with Debian dir 74c1020476ae60d43d9e650846920be50706f620
- - - - -
21cbf964 by Hilmar Preuße at 2026-08-01T20:28:43+02:00
Release 0.3-1.

- - - - -


9 changed files:

- .github/workflows/regressions.yml
- debian/changelog
- mod_procfs.c
- mod_procfs.html
- t/lib/ProFTPD/Tests/Modules/mod_procfs.pm
- + t/lib/ProFTPD/Tests/Modules/mod_procfs/digest.pm
- t/lib/ProFTPD/Tests/Modules/mod_procfs/sftp.pm
- + t/modules/mod_procfs/digest.t
- tests.pl


Changes:

=====================================
.github/workflows/regressions.yml
=====================================
@@ -90,7 +90,7 @@ jobs:
         # NOTE: Docker does not have good IPv6 support, hence we disable it.
         run: |
           cd proftpd
-          ./configure --disable-ipv6 --with-modules=mod_site_misc:mod_copy:mod_sftp:mod_procfs
+          ./configure --disable-ipv6 --with-modules=mod_site_misc:mod_copy:mod_digest:mod_sftp:mod_procfs
           make
           ./proftpd -V
           make install


=====================================
debian/changelog
=====================================
@@ -1,3 +1,10 @@
+proftpd-mod-procfs (0.3-1) unstable; urgency=medium
+
+  * New upstream release.
+    - Prevents access to 'sysfs' file system too.
+
+ -- Hilmar Preuße <hille42 at debian.org>  Sat, 01 Aug 2026 10:44:07 +0200
+
 proftpd-mod-procfs (0.2-1) unstable; urgency=medium
 
   * New upstream release.


=====================================
mod_procfs.c
=====================================
@@ -52,7 +52,7 @@
 # error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
-#define MOD_PROCFS_VERSION	"mod_procfs/0.2"
+#define MOD_PROCFS_VERSION	"mod_procfs/0.3"
 
 module procfs_module;
 
@@ -65,6 +65,7 @@ static const char *trace_channel = "procfs";
 struct procfs_mount {
   const char *path;
   size_t path_len;
+  const char *type;
 };
 
 static array_header *procfs_mounts = NULL;
@@ -86,20 +87,28 @@ static int get_procfs_mounts(pool *p) {
 
   mnt = getmntent(mountf);
   while (mnt != NULL) {
+    struct procfs_mount *mount = NULL;
+    size_t path_len = 0;
+
     pr_signals_handle();
 
     if (strcmp(mnt->mnt_type, "proc") == 0) {
-      struct procfs_mount *mount;
-      size_t path_len;
-
       pr_log_debug(DEBUG0, MOD_PROCFS_VERSION
         ": discovered procfs mounted at '%s'", mnt->mnt_dir);
+      mount = palloc(p, sizeof(struct procfs_mount));
+      mount->type = pstrdup(p, "procfs");
 
+    } else if (strcmp(mnt->mnt_type, "sysfs") == 0) {
+      pr_log_debug(DEBUG0, MOD_PROCFS_VERSION
+        ": discovered sysfs mounted at '%s'", mnt->mnt_dir);
       mount = palloc(p, sizeof(struct procfs_mount));
+      mount->type = pstrdup(p, "sysfs");
+    }
 
+    if (mount != NULL) {
       /* If the mount point path does not end with a trailing slash, add it.
-       * We use this property when checking paths that reference this procfs
-       * mount point.
+       * We use this property when checking paths that reference this mount
+       * point.
        */
       path_len = strlen(mnt->mnt_dir);
       if (mnt->mnt_dir[path_len-1] != '/') {
@@ -163,7 +172,7 @@ static int get_procfs_mounts(pool *p) {
   return 0;
 }
 
-static int is_procfs_path(pool *p, const char *path) {
+static int is_blocked_path(pool *p, const char *path) {
   register unsigned int i;
   int res = FALSE;
   char *abs_path;
@@ -182,7 +191,8 @@ static int is_procfs_path(pool *p, const char *path) {
     mount = mounts[i];
 
     pr_trace_msg(trace_channel, 19,
-      "checking path '%s' against procfs mount '%s'", abs_path, mount->path);
+      "checking path '%s' against %s mount '%s'", abs_path, mount->type,
+      mount->path);
 
     if (abs_pathlen >= mount->path_len &&
         strncmp(abs_path, mount->path, mount->path_len) == 0) {
@@ -253,7 +263,7 @@ static modret_t *handle_path(cmd_rec *cmd, const char *cmd_name,
     const char *path) {
   pr_trace_msg(trace_channel, 19, "checking path '%s' for %s", path, cmd_name);
 
-  if (is_procfs_path(cmd->tmp_pool, path) == TRUE) {
+  if (is_blocked_path(cmd->tmp_pool, path) == TRUE) {
     const char *proto, *resp_code;
 
     proto = pr_session_get_protocol(0);
@@ -421,6 +431,42 @@ MODRET procfs_sftp_pre_path(cmd_rec *cmd) {
   return handle_path(cmd, cmd->argv[0], path);
 }
 
+MODRET procfs_sftp_pre_hardlink(cmd_rec *cmd) {
+  const char *src_path, *dst_path, *proto;
+  char *ptr;
+  modret_t *mr;
+
+  if (procfs_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  proto = pr_session_get_protocol(0);
+  if (strcmp(proto, "sftp") != 0) {
+    return PR_DECLINED(cmd);
+  }
+
+  /* Unfortunately, mod_sftp currently does NOT break the two paths into
+   * the cmd->argv array; it only populates cmd->arg.
+   *
+   * In the future, if/when mod_sftp behavior changes, we can look at the
+   * cmd->argc to determine which style is being used by mod_sftp.
+   */
+  ptr = strchr(cmd->arg, ' ');
+  if (ptr == NULL) {
+    return PR_DECLINED(cmd);
+  }
+
+  src_path = pstrndup(cmd->tmp_pool, cmd->arg, ptr - cmd->arg);
+  dst_path = pstrdup(cmd->tmp_pool, ptr + 1);
+
+  mr = handle_path(cmd, cmd->argv[0], src_path);
+  if (MODRET_ISERROR(mr)) {
+    return mr;
+  }
+
+  return handle_path(cmd, cmd->argv[0], dst_path);
+}
+
 MODRET procfs_sftp_pre_symlink(cmd_rec *cmd) {
   const char *src_path, *dst_path, *proto;
   char *ptr;
@@ -639,7 +685,19 @@ static cmdtable procfs_cmdtab[] = {
   { PRE_CMD,		C_STAT,	G_NONE,	procfs_pre_path,	FALSE, FALSE },
   { PRE_CMD,		C_STOR,	G_NONE,	procfs_pre_path,	FALSE, FALSE },
 
+  /* mod_digest FTP commands */
+  { PRE_CMD,	"HASH",		G_NONE,	procfs_pre_path,	FALSE, FALSE },
+  { PRE_CMD,	"MD5",		G_NONE,	procfs_pre_path,	FALSE, FALSE },
+  { PRE_CMD,	"XCRC",		G_NONE,	procfs_pre_path,	FALSE, FALSE },
+  { PRE_CMD,	"XMD5",		G_NONE,	procfs_pre_path,	FALSE, FALSE },
+  { PRE_CMD,	"XSHA",		G_NONE,	procfs_pre_path,	FALSE, FALSE },
+  { PRE_CMD,	"XSHA1",	G_NONE,	procfs_pre_path,	FALSE, FALSE },
+  { PRE_CMD,	"XSHA256",	G_NONE,	procfs_pre_path,	FALSE, FALSE },
+  { PRE_CMD,	"XSHA512",	G_NONE,	procfs_pre_path,	FALSE, FALSE },
+
   /* SFTP */
+  { PRE_CMD, "HARDLINK",	G_NONE, procfs_sftp_pre_hardlink, FALSE, FALSE },
+  { PRE_CMD, "LINK",		G_NONE, procfs_sftp_pre_hardlink, FALSE, FALSE },
   { PRE_CMD, "LSTAT",		G_NONE, procfs_sftp_pre_path, FALSE, FALSE },
   { PRE_CMD, "OPENDIR",		G_NONE, procfs_sftp_pre_path, FALSE, FALSE },
   { PRE_CMD, "READLINK",	G_NONE, procfs_sftp_pre_path, FALSE, FALSE },


=====================================
mod_procfs.html
=====================================
@@ -14,7 +14,7 @@
 
 <p>
 The <code>mod_procfs</code> module affects the visibility and usage of the
-<code>/proc</code> filesystem directory.
+<code>/proc</code> and <code>/sys</code> pseudo-filesystem directories.
 
 <p>
 This module is contained in the <code>mod_procfs</code> file for
@@ -49,17 +49,18 @@ questions, concerns, or suggestions regarding this module.
 
 <p>
 The <code>ProcfsEngine</code> directive enables or disables the module's
-hiding of the <code>/proc</code> filesystem.  When enabled,
-<code>mod_procfs</code> works to prevent access to the <code>/proc</code>
-filesystem, returning "No such file or directory" errors for such access
-attempts.
+hiding of the <code>/proc</code> and <code>/sys</code> filesystems.  When
+enabled, <code>mod_procfs</code> works to prevent access to the
+<code>/proc</code> and <code>/sys</code> pseudo-filesystems, returning
+"No such file or directory" errors for such access attempts.
 
 <p>
 <b>Note</b> that <code>ProcfsEngine</code> is <em>automatically enabled</em>
-if the module detects, on startup, that the <code>/proc</code> filesystem
-is present.  If the <code>/proc</code> filesystem is not present on startup,
-then <code>ProcfsEngine</code> is automatically disabled.  Explicitly setting
-this directive overrides the automatic behavior.
+if the module detects, on startup, that the <code>/proc</code> or
+<code>/sys</code> filesystems are present.  If the <code>/proc</code> and
+<code>/sys</code> filesystems are not present on startup, then
+<code>ProcfsEngine</code> is automatically disabled.  Explicitly setting this
+directive overrides the automatic behavior.
 
 <hr>
 <h2><a name="ProcfsLog">ProcfsLog</a></h2>
@@ -121,8 +122,8 @@ your existing server:
 <p>
 The <code>mod_procfs</code> module works by using the ProFTPD command dispatch
 system to intercept all commands/requests that use paths; if the paths
-being used reference the <code>/proc</code> filesystem, those commands will
-fail with a "No such file or directory" error.
+being used reference the <code>/proc</code> or <code>/sys</code> filesystems,
+those commands will fail with a "No such file or directory" error.
 
 <p>
 Example configuration:


=====================================
t/lib/ProFTPD/Tests/Modules/mod_procfs.pm
=====================================
@@ -83,7 +83,12 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  procfs_retr_rejected => {
+  procfs_retr_procfs_rejected => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_retr_sysfs_rejected => {
     order => ++$order,
     test_class => [qw(forking)],
   },
@@ -1792,7 +1797,7 @@ sub procfs_nlst_rejected {
   test_cleanup($setup, $ex);
 }
 
-sub procfs_retr_rejected {
+sub procfs_retr_procfs_rejected {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'procfs');
@@ -1885,6 +1890,131 @@ sub procfs_retr_rejected {
   test_cleanup($setup, $ex);
 }
 
+sub procfs_retr_sysfs_rejected {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/sys/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'command:10 response:10 procfs:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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 {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw($test_file);
+      if ($conn) {
+        die("RETR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /RETR $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
 sub procfs_rmd_rejected {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};


=====================================
t/lib/ProFTPD/Tests/Modules/mod_procfs/digest.pm
=====================================
@@ -0,0 +1,1151 @@
+package ProFTPD::Tests::Modules::mod_procfs::digest;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Cwd;
+use File::Copy;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use IPC::Open3;
+use Socket;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  procfs_digest_hash => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_digest_md5 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_digest_xmd5 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_digest_xcrc => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_digest_xsha => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_digest_xsha1 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_digest_xsha256 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  procfs_digest_xsha512 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+#    procfs_digest_xsha
+#    procfs_digest_xsha1
+#    procfs_digest_xsha256
+}
+
+sub procfs_digest_hash {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('HASH', $test_file) };
+      unless ($@) {
+        die("HASH command succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /HASH $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+sub procfs_digest_md5 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('MD5', $test_file) };
+      unless ($@) {
+        die("MD5 command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /MD5 $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+sub procfs_digest_xmd5 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XMD5', $test_file) };
+      unless ($@) {
+        die("XMD5 command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /XMD5 $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+sub procfs_digest_xcrc {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XCRC', $test_file) };
+      unless ($@) {
+        die("XCRC command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /XCRC $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+sub procfs_digest_xsha {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XSHA', $test_file) };
+      unless ($@) {
+        die("XSHA command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /XSHA $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+sub procfs_digest_xsha1 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XSHA1', $test_file) };
+      unless ($@) {
+        die("XSHA1 command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /XSHA1 $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+sub procfs_digest_xsha256 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XSHA256', $test_file) };
+      unless ($@) {
+        die("XSHA256 command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /XSHA256 $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+sub procfs_digest_xsha512 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $test_file = '/proc/test.txt';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XSHA512', $test_file) };
+      unless ($@) {
+        die("XSHA512 command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /XSHA512 $test_file denied by mod_procfs/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
+1;


=====================================
t/lib/ProFTPD/Tests/Modules/mod_procfs/sftp.pm
=====================================
@@ -84,6 +84,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  procfs_sftp_ext_hardlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   # TODO
   # procfs_sftp_link
 };
@@ -2202,4 +2207,215 @@ sub procfs_sftp_symlink {
   test_cleanup($setup, $ex);
 }
 
+sub procfs_sftp_ext_hardlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'procfs');
+
+  my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key");
+  my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key");
+
+  my $rsa_priv_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/test_rsa_key");
+  my $rsa_pub_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/test_rsa_key.pub");
+  my $rsa_rfc4716_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/authorized_rsa_keys");
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $from_file = '/proc/src.txt';
+  my $to_file = '/proc/dst.txt';
+
+  # As Net::SSH2::SFTP (via libssh2) does not implement LINK, we'll use
+  # OpenSSH's sftp(1) and its "hardlink at openssh.com" extension.
+
+  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.conf");
+  if (open(my $fh, "> $batch_file")) {
+    print $fh "ln $from_file $to_file\n";
+
+    unless (close($fh)) {
+      die("Can't write $batch_file: $!");
+    }
+
+  } else {
+    die("Can't open $batch_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'procfs:20 sftp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_procfs.c' => {
+        ProcfsEngine => 'on',
+        ProcfsLog => $setup->{log_file},
+      },
+
+      'mod_sftp.c' => [
+        'SFTPEngine on',
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+      ],
+    },
+  };
+
+  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: $!");
+  }
+
+  require Net::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow for server startup
+      sleep(1);
+
+      my $sftp = 'sftp';
+
+      my @cmd = (
+        $sftp,
+        '-oBatchMode=yes',
+        '-oCheckHostIP=no',
+        '-oCompression=yes',
+        "-oPort=$port",
+        "-oIdentityFile=$rsa_priv_key",
+        '-oIdentitiesOnly=yes',
+        '-oPubkeyAuthentication=yes',
+        '-oStrictHostKeyChecking=no',
+        '-oUserKnownHostsFile=/dev/null',
+        '-vvv',
+        '-b',
+        $batch_file,
+        "$setup->{user}\@127.0.0.1",
+      );
+
+      my $sftp_rh = IO::Handle->new();
+      my $sftp_wh = IO::Handle->new();
+      my $sftp_eh = IO::Handle->new();
+
+      $sftp_wh->autoflush(1);
+
+      sleep(1);
+
+      local $SIG{CHLD} = 'DEFAULT';
+
+      # Make sure that the perms on the priv key are what OpenSSH wants
+      unless (chmod(0400, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0400: $!");
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
+      waitpid($sftp_pid, 0);
+      my $exit_status = $?;
+
+      # Restore the perms on the priv key
+      unless (chmod(0644, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      }
+
+      my ($res, $errstr);
+      if ($exit_status >> 8 == 0) {
+        $errstr = join('', <$sftp_eh>);
+        $res = 0;
+
+      } else {
+        $errstr = join('', <$sftp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
+        $res = 1;
+      }
+
+      if ($res == 0) {
+        die("Hardlink $from_file on server succeeded unexpectedly");
+      }
+    };
+    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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        next unless $line =~ /mod_procfs\//;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /HARDLINK $from_file denied by mod_procfs/) {
+          $ok = 1;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected ProcfsLog messages"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup, $ex);
+}
+
 1;


=====================================
t/modules/mod_procfs/digest.t
=====================================
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+
+use lib qw(t/lib);
+use strict;
+
+use Test::Unit::HarnessUnit;
+
+$| = 1;
+
+my $r = Test::Unit::HarnessUnit->new();
+$r->start("ProFTPD::Tests::Modules::mod_procfs::digest");


=====================================
tests.pl
=====================================
@@ -62,6 +62,11 @@ if (scalar(@ARGV) > 0) {
   my $order = 0;
 
   my $FEATURE_TESTS = {
+    't/modules/mod_procfs/digest.t' => {
+      order => ++$order,
+      test_class => [qw(mod_digest)],
+    },
+
     't/modules/mod_procfs/sftp.t' => {
       order => ++$order,
       test_class => [qw(mod_sftp)],



View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-procfs/-/compare/0e6f324dc3603fdc821dd6d3328a8280c5fc10d1...21cbf9648003193e74e1fb62acaf9bcb869e53f5

-- 
View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-procfs/-/compare/0e6f324dc3603fdc821dd6d3328a8280c5fc10d1...21cbf9648003193e74e1fb62acaf9bcb869e53f5
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help




More information about the Pkg-proftpd-maintainers mailing list