diff --git a/Dockerfile b/Dockerfile
index 8615be52fb07336082dd21282b2c00fd65bfac6c..d56437a470709cc32dbfffc36527dd69d1aa6d3f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,11 +4,8 @@ FROM debian:stable
ARG MAXIMA_VERSION
# e.g. 2.0.22.0.2
ARG SBCL_VERSION
-# e.g. assStackQuestion/classes/stack/maxima
-ARG LIB_PATH
-RUN echo ${LIB_PATH?Error \$LIB_PATH is not defined} \
- ${MAXIMA_VERSION?Error \$MAXIMA_VERSION is not defined} \
+RUN echo ${MAXIMA_VERSION?Error \$MAXIMA_VERSION is not defined} \
${SBCL_VERSION?Error \$SBCL_VERSION is not defined}
ENV SRC=/opt/src \
@@ -28,6 +25,7 @@ RUN apt-get update \
make \
wget \
python3 \
+ gcc \
# ca-certificates \
# curl \
texinfo
@@ -55,16 +53,28 @@ RUN cd ${SRC} \
&& make install \
&& make clean
+
+RUN apt-get install -y gnuplot gettext-base sudo psmisc libbsd-dev tini
+
+COPY ./src/maxima_fork.c ${SRC}
+
+RUN cd ${SRC} && gcc -shared maxima_fork.c -lbsd -fPIC -Wall -Wextra -o libmaximafork.so \
+ && mv libmaximafork.so /usr/lib
+
RUN rm -r ${SRC} /SBCL_ARCH
-RUN apt-get install -y gnuplot gettext-base sudo psmisc
RUN mkdir -p ${LIB} ${LOG} ${TMP} ${PLOT} ${ASSETS} ${BIN}
+
+# e.g. assStackQuestion/classes/stack/maxima
+ARG LIB_PATH
+
+RUN echo ${LIB_PATH?Error \$LIB_PATH is not defined}
# Copy Libraries
COPY ${LIB_PATH} ${LIB}
# Copy optimization scripts
-COPY assets/optimize.mac.template assets/maximalocal.mac.template ${ASSETS}/
+COPY assets/maxima-fork.lisp assets/optimize.mac.template assets/maximalocal.mac.template ${ASSETS}/
RUN grep stackmaximaversion ${LIB}/stackmaxima.mac | grep -oP "\d+" >> /opt/maxima/stackmaximaversion \
&& sh -c 'envsubst < ${ASSETS}/maximalocal.mac.template > ${ASSETS}/maximalocal.mac \
@@ -74,19 +84,14 @@ RUN grep stackmaximaversion ${LIB}/stackmaxima.mac | grep -oP "\d+" >> /opt/maxi
&& maxima -b optimize.mac \
&& mv maxima-optimised ${BIN}/maxima-optimised
-RUN apt-get purge -y wget python3 make bzip2 texinfo
+RUN apt-get purge -y wget python3 make bzip2 texinfo gcc
-RUN useradd -M maxima-server && echo "Defaults lecture = always" > /etc/sudoers.d/maxima
RUN for i in $(seq 16); do \
- useradd -M "maxima-$i" \
- && echo "maxima-server ALL = (maxima-$i) NOPASSWD: ${BIN}/wrapper" >> /etc/sudoers.d/maxima \
- && echo "maxima-server ALL = (root) NOPASSWD: /usr/bin/killall -9 -u maxima-$i" >> /etc/sudoers.d/maxima; \
+ useradd -M "maxima-$i"; \
done
# Add go webserver
COPY ./bin/web ${BIN}/goweb
-# Add wrapper
-COPY ./bin/wrapper ${BIN}/wrapper
-CMD ["su", "-c", "/opt/maxima/bin/goweb", "maxima-server"]
+CMD rm /dev/tty && exec tini ${BIN}/goweb
diff --git a/assets/maxima-fork.lisp b/assets/maxima-fork.lisp
new file mode 100644
index 0000000000000000000000000000000000000000..0c408b43e3996c3ad79a62bf3fd0d466e4708bbb
--- /dev/null
+++ b/assets/maxima-fork.lisp
@@ -0,0 +1,29 @@
+(cl:in-package "MAXIMA")
+
+(defun set-tmp-dir-vars (tmp-dir)
+ (defparameter |$image_dir| (concatenate 'string tmp-dir "/output/"))
+ (defparameter |$MAXIMA_TEMPDIR| (concatenate 'string tmp-dir "/work/"))
+ nil)
+(defparameter |$url_base| "!ploturl!")
+
+(cl:defpackage "MAXIMA-FORK"
+ (:use "CL" "SB-ALIEN")
+ (:export "FORKING-LOOP"))
+(cl:in-package "MAXIMA-FORK")
+
+;;; load shared library
+(load-shared-object "libmaximafork.so")
+
+;;; define c function
+(declaim (inline fork-new-process))
+(define-alien-routine fork-new-process c-string)
+
+;;; forking loop
+(defun forking-loop ()
+ (finish-output)
+ (let ((tmp-dir (fork-new-process)))
+ (when (not tmp-dir)
+ (sb-ext:exit :code 1))
+ (maxima::set-tmp-dir-vars tmp-dir))
+ t)
+
diff --git a/assets/optimize.mac.template b/assets/optimize.mac.template
index 753db1144f13db60a21f833d17473799fd9b23a7..bb78dbf17c58fda7e617eed9e79faf164796d7f3 100644
--- a/assets/optimize.mac.template
+++ b/assets/optimize.mac.template
@@ -3,6 +3,7 @@
https://github.com/maths/moodle-qtype_stack/blob/master/doc/en/CAS/Optimising_Maxima.md */
load("${ASSETS}/maximalocal.mac");
+load("${ASSETS}/maxima-fork.lisp");
load("${LIB}/stackmaxima.mac");
load(stats);
load(distrib);
diff --git a/src/maxima_fork.c b/src/maxima_fork.c
new file mode 100644
index 0000000000000000000000000000000000000000..7eb457380438aa39295d500c02164d234498d958
--- /dev/null
+++ b/src/maxima_fork.c
@@ -0,0 +1,212 @@
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <pwd.h>
+#include <signal.h>
+
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <bsd/unistd.h>
+#include <limits.h>
+#include <grp.h>
+#define N_SLOT 16
+#define RNOFILE 256
+#define FILEPATH_LEN (PATH_MAX + 1)
+char filepath[FILEPATH_LEN];
+
+// inits a maxima process for web service:
+// changes gid/uid to maxima-{slot}
+// redirects input/output, creates temporary subdirectories
+char *fork_new_process() {
+ fflush(stdout);
+
+ // send an S for Synchronization, so that
+ // the server process doesn't accidentally write into
+ // sbcl's buffer
+ // the server should not write anything before it has read this
+ write(STDOUT_FILENO, "S", 1);
+
+ // while the loop is running, the SIGCHLD handler
+ // is deactivated so that children are automatically reaped
+ // after that, it is again restored
+ struct sigaction old, new;
+ new.sa_handler = SIG_IGN;
+ sigemptyset(&new.sa_mask);
+ new.sa_flags = SA_NOCLDWAIT;
+ char *ret = NULL;
+ if (sigaction(SIGCHLD, &new, &old) == -1) {
+ perror("Could not set signal error for children");
+ return NULL;
+ }
+
+ // when sbcl spawns a child process through lisp, sbcl tries to close all
+ // filedescriptors until RLIMIT_NOFILE
+ // in docker containers, this is by standard quite high, so it takes long
+ // which is remediated here by setting it lower manually
+ struct rlimit nofile = { .rlim_cur = RNOFILE, .rlim_max = RNOFILE };
+ if (setrlimit(RLIMIT_NOFILE, &nofile) == -1) {
+ perror("Error setting rlimit_nofile");
+ sigaction(SIGCHLD, &old, NULL);
+ return NULL;
+ }
+
+ for (;;) {
+ // can't flush enough
+ fflush(stdout);
+ int slot;
+
+ // the slot number and temp directory is sent to the process
+ // over stdin in the format "%d%s", where %s can contain anything
+ // but newlines and musn't start with a number, which isn't a
+ // problem for absolute paths
+ if (scanf("%d", &slot) == EOF) {
+ if (errno != 0) {
+ perror("Error getting slot number from stdin");
+ ret = NULL;
+ }
+ break;
+ }
+ char *tempdir = fgets(filepath, FILEPATH_LEN, stdin);
+ if (!tempdir) {
+ perror("Error getting temp path name");
+ ret = NULL;
+ break;
+ }
+ // remove the last newline, if it exists
+ size_t last_char = strlen(tempdir) - 1;
+ if (tempdir[last_char] == '\n') {
+ tempdir[strlen(tempdir) - 1] = '\0';
+ }
+
+ // we fork the main process and use the child without execve
+ // this way, startup time is improved
+ pid_t pid = fork();
+ if (pid == -1) {
+ perror("Could not fork");
+ ret = NULL;
+ break;
+ }
+ if (pid != 0) {
+ continue;
+ }
+
+ if (chdir(tempdir) == -1) {
+ perror("Could not chdir to temporary directory");
+ ret = NULL;
+ break;
+ }
+
+ // redirect stdout to pipe
+ // note: open outpipe before inpipe to avoid deadlock
+ int outfd = open("outpipe", O_WRONLY);
+ if (outfd == -1) {
+ perror("Could not connect output pipe");
+ ret = NULL;
+ break;
+ }
+ if (dup2(outfd, STDOUT_FILENO) == -1) {
+ perror("Could not copy output file descriptor");
+ ret = NULL;
+ break;
+ }
+
+ // redirect stdin from pipe
+ int infd = open("inpipe", O_RDONLY);
+ if (infd == -1) {
+ perror("Could not create input pipe");
+ ret = NULL;
+ break;
+ }
+ if (dup2(infd, STDIN_FILENO) == -1) {
+ perror("Could not copy input file descriptor");
+ ret = NULL;
+ break;
+ }
+
+ // replace stdin with a new stream, for good measure
+ FILE *new_stdin = fdopen(STDIN_FILENO, "r");
+ if (!new_stdin) {
+ perror("Could not create stream from stdin");
+ ret = NULL;
+ break;
+ }
+ stdin = new_stdin;
+
+ // everything execpt std{in,out,err} is closed
+ // note: this is a function from libbsd
+ closefrom(3);
+
+ // get uid/gid from username
+ if (slot <= 0 || slot > N_SLOT) {
+ dprintf(STDERR_FILENO, "Invalid slot number: %d\n", slot);
+ ret = NULL;
+ break;
+ }
+ char username[16];
+ int len = snprintf(username, 15, "maxima-%d", slot);
+ if (len < 0 || len > 15) {
+ dprintf(STDERR_FILENO, "Internal error getting user name\n");
+ ret = NULL;
+ break;
+ }
+ struct passwd *userinfo = getpwnam(username);
+ if (!userinfo) {
+ dprintf(STDERR_FILENO, "Could not read user information for %s: %s\n",
+ username, strerror(errno));
+ ret = NULL;
+ break;
+ }
+ uid_t uid = userinfo->pw_uid;
+ gid_t gid = userinfo->pw_gid;
+ if (uid == 0 || gid == 0) {
+ dprintf(STDERR_FILENO, "Refusing to setuid/gid to root\n");
+ ret = NULL;
+ break;
+ }
+
+ // note: setgid should be executed before setuid when dropping from root
+ if (setgid(gid) == -1) {
+ perror("Could not set gid");
+ ret = NULL;
+ break;
+ }
+
+ // remove all aux groups
+ if (setgroups(0, NULL)) {
+ perror("Could not remove aux groups");
+ ret = NULL;
+ break;
+ }
+
+ // after this, we should be non-root
+ if (setuid(uid) == -1) {
+ perror("Could not set uid");
+ ret = NULL;
+ break;
+ }
+
+ // create temporary folders and files
+ if (mkdir("output", 0770) == -1) {
+ perror("Could not create output directory");
+ ret = NULL;
+ break;
+ }
+ if (mkdir("work", 0770) == -1) {
+ perror("Could not create work directory");
+ ret = NULL;
+ break;
+ }
+ ret = tempdir;
+ break;
+ }
+ // restore normal SIGCHLD handler
+ if (sigaction(SIGCHLD, &old, NULL) == -1) {
+ return NULL;
+ }
+ return ret;
+}