blob: bbbf7392de49ddeb77d3a03ec8d948869e30db29 [file] [log] [blame]
From 77f549b03ecde43e926482092ecad6cd6f3cda7b Mon Sep 17 00:00:00 2001
From: Mike Frysinger <vapier@chromium.org>
Date: Sat, 12 Dec 2015 15:18:25 -0500
Subject: [PATCH] do not source/exec scripts on noexec mount points
Today, if you have a script that lives on a noexec mount point, the
kernel will reject attempts to run it directly:
$ printf '#!/bin/sh\necho hi\n' > /dev/shm/test.sh
$ chmod a+rx /dev/shm/test.sh
$ /dev/shm/test.sh
bash: /dev/shm/test.sh: Permission denied
But bash itself has no problem running this file:
$ bash /dev/shm/test.sh
hi
Or with letting other scripts run this file:
$ bash -c '. /dev/shm/test.sh'
hi
Or with reading the script from stdin:
$ bash </dev/shm/test.sh
hi
Or indirect loading:
$ ln -s test.sh /dev/shm/.profile
$ HOME=/dev/shm bash -l
hi
This detracts from the security of the overall system. People writing
scripts sometimes want to save/restore state (like variables) and will
restore the content from a noexec point using the aforementioned source
command without realizing that it executes code too. Of course their
code is wrong, but it would be nice if the system would catch & reject
it explicitly to stave of inadvertent usage.
This is not a perfect solution as it can still be worked around by
inlining the code itself:
$ bash -c "$(cat /dev/shm/test.sh)"
hi
Or forcing interactive mode:
$ bash -i </dev/shm/test.sh
hi
Or piping it:
$ cat /dev/shm/test.sh | bash
hi
But this makes things a bit harder for malicious attackers (depending
how exactly they've managed to escalate), and it also helps developers
avoid getting it wrong in the first place.
There are some compile-time knobs provided:
* SHELL_IGNORE_NOEXEC: If defined, allow scripts on noexec mounts.
* SHELL_NOEXEC_CRASH_REPORTS: If defined, generate crash reports when
noexec scripts are attempted.
* SHELL_NOEXEC_REPORT_ONLY: If defined, don't halt script execution,
only emit warnings to stderr.
URL: https://crbug.com/569168
URL: https://chromium.googlesource.com/chromiumos/docs/+/master/security/noexec_shell_scripts.md
---
builtins/evalfile.c | 24 ++++++++++++++++++++++++
config.h.in | 3 +++
configure | 2 +-
configure.ac | 2 +-
shell.c | 31 +++++++++++++++++++++++++++++++
5 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/builtins/evalfile.c b/builtins/evalfile.c
index 4539b637ded4..f7e03d808262 100644
--- a/builtins/evalfile.c
+++ b/builtins/evalfile.c
@@ -32,6 +32,10 @@
#include <signal.h>
#include <errno.h>
+#if defined (HAVE_SYS_STATVFS_H)
+# include <sys/statvfs.h>
+#endif
+
#include "../bashansi.h"
#include "../bashintl.h"
@@ -155,6 +159,9 @@ file_error_and_exit:
return ((flags & FEVAL_BUILTIN) ? EXECUTION_FAILURE : -1);
}
+ if (interactive_shell == 0)
+ check_noexec (fd, filename);
+
if (S_ISREG (finfo.st_mode) && file_size <= SSIZE_MAX)
{
string = (char *)xmalloc (1 + file_size);
diff --git a/config.h.in b/config.h.in
index 1e4b71d1e7fe..4c7e5c605524 100644
--- a/config.h.in
+++ b/config.h.in
@@ -1052,6 +1052,9 @@
/* Define if you have the <sys/stat.h> header file. */
#undef HAVE_SYS_STAT_H
+/* Define if you have <sys/statvfs.h>. */
+#undef HAVE_SYS_STATVFS_H
+
/* Define if you have the <sys/stream.h> header file. */
#undef HAVE_SYS_STREAM_H
diff --git a/configure b/configure
index b9c012961cf2..5457d87be2f4 100755
--- a/configure
+++ b/configure
@@ -9289,7 +9289,7 @@ fi
# On IRIX 5.3, sys/types and inttypes.h are conflicting.
for ac_header in sys/types.h sys/stat.h stdlib.h string.h memory.h strings.h \
- inttypes.h stdint.h unistd.h
+ inttypes.h stdint.h unistd.h sys/statvfs.h
do :
as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh`
ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default
diff --git a/configure.ac b/configure.ac
index 5080265a96b0..351cabbb0c8c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -711,7 +711,7 @@ AC_CHECK_HEADERS(unistd.h stdlib.h stdarg.h varargs.h limits.h string.h \
stdbool.h stddef.h stdint.h netdb.h pwd.h grp.h strings.h \
regex.h syslog.h ulimit.h)
AC_CHECK_HEADERS(sys/pte.h sys/stream.h sys/select.h sys/file.h \
- sys/resource.h sys/param.h sys/socket.h sys/stat.h \
+ sys/resource.h sys/param.h sys/socket.h sys/stat.h sys/statvfs.h \
sys/time.h sys/times.h sys/types.h sys/wait.h)
AC_CHECK_HEADERS(netinet/in.h arpa/inet.h)
diff --git a/shell.c b/shell.c
index 0179e1e4512b..7d313d671b21 100644
--- a/shell.c
+++ b/shell.c
@@ -46,6 +46,10 @@
# include <unistd.h>
#endif
+#if defined (HAVE_SYS_STATVFS_H)
+# include <sys/statvfs.h>
+#endif
+
#include "bashintl.h"
#define NEED_SH_SETLINEBUF_DECL /* used in externs.h */
@@ -747,11 +751,14 @@ main (argc, argv, env)
else if (interactive == 0)
/* In this mode, bash is reading a script from stdin, which is a
pipe or redirected file. */
+ {
+ check_noexec (0, "stdin");
#if defined (BUFFERED_INPUT)
default_buffered_input = fileno (stdin); /* == 0 */
#else
setbuf (default_input, (char *)NULL);
#endif /* !BUFFERED_INPUT */
+ }
set_bash_input ();
@@ -1483,6 +1490,63 @@ start_debugger ()
#endif
}
+#ifndef SHELL_IGNORE_NOEXEC
+
+/*
+ * We'll fork a child who will then crash. This will signal to the system
+ * that we ran into a problem without actually halting the script. This is
+ * useful for tracking down users on releases w/out breaking them.
+ */
+static void
+maybe_generate_crash_report (void)
+{
+# ifdef SHELL_NOEXEC_CRASH_REPORTS
+ if (fork () == 0)
+ abort ();
+# endif
+}
+
+/*
+ * See if the fd is coming from a noexec partition.
+ * If so, fall over and complain.
+ */
+void
+check_noexec (int fd, const char *source)
+{
+#if defined (HAVE_SYS_STATVFS_H) && defined (ST_NOEXEC)
+ /* Make sure the file isn't on a noexec mount point. */
+ struct statvfs stvfs;
+
+ if (fstatvfs (fd, &stvfs) == -1)
+ {
+ maybe_generate_crash_report ();
+
+ sys_error ("Can't fstatvfs %s", source);
+# ifdef SHELL_NOEXEC_REPORT_ONLY
+ /* Clear the flag to avoid the code path below. */
+ stvfs.f_flag = 0;
+# else
+ exit_shell (EX_NOTFOUND);
+# endif
+ }
+
+ if (stvfs.f_flag & ST_NOEXEC)
+ {
+ const char docs[] = "https://chromium.googlesource.com/chromiumos/docs/+/master/security/noexec_shell_scripts.md";
+ maybe_generate_crash_report ();
+
+# ifdef SHELL_NOEXEC_REPORT_ONLY
+ internal_warning ("%s: warning: script from noexec mount; see %s", source, docs);
+# else
+ internal_error ("Refusing to exec %s from noexec mount; see %s", source, docs);
+ exit_shell (EX_NOEXEC);
+# endif
+ }
+#endif
+}
+
+#endif
+
static int
open_shell_script (script_name)
char *script_name;
@@ -1517,6 +1582,8 @@ open_shell_script (script_name)
SET_CLOSE_ON_EXEC (fileno (default_input));
#endif /* !BUFFERED_INPUT */
+ check_noexec (fd, filename);
+
/* Just about the only way for this code to be executed is if something
like `bash -i /dev/stdin' is executed. */
if (interactive_shell && fd_is_tty)
--- a/shell.h
+++ b/shell.h
@@ -184,3 +184,9 @@ extern void restore_parser_state __P((sh_parser_state_t *));
extern sh_input_line_state_t *save_input_line_state __P((sh_input_line_state_t *));
extern void restore_input_line_state __P((sh_input_line_state_t *));
+
+#ifndef SHELL_IGNORE_NOEXEC
+extern void check_noexec __P((int, const char *));
+#else
+static inline void check_noexec (int fd, const char *source) {}
+#endif
--
2.12.0