commit f6eca21e35a8128be173496553bea71323f8b916
parent 099ce388993818f600c4e41172f71e20520dce7e
Author: Rogo <goryachev.romochka@gmail.com>
Date: Thu, 12 Feb 2026 16:20:29 +0300
[quark][patches][noroot] noroot patch is more usable and safe now
Add quark-noroot-20260211-5ad0df9.diff patch which makes quark safe and
completly usable without root or special priviliges.
Fixes:
* Make internal path relative to the current working directory or matched
vhost ones.
* Remove process-related calls, because process capabilities and
credentials better be edited before executing quark anyway, and
setpgrp(2) breaks shell usability.
Diffstat:
2 files changed, 416 insertions(+), 3 deletions(-)
diff --git a/tools.suckless.org/quark/patches/noroot/index.md b/tools.suckless.org/quark/patches/noroot/index.md
@@ -5,17 +5,36 @@ Description
-----------
This patch removes the necessity and ability to run quark as root. quark
will neither chroot(2) into the serving directory nor change the UID,
-GID or ownership of the UNIX-domain socket file.
+GID or ownership of the UNIX-domain socket file. It will only serve
+files under its current working directory; absolute paths and ".." are
+normalized into relative ones to the current working directory or
+vhost ones.
-As this patch removes security features from quark, it should not be
-used for serving content to untrusted parties.
+Process related functions, like 'setpgrp(2)' and 'setrlimit(2)', are also
+removed because your system already has better tools to manage process
+credentials and capabilities. For example, to bind quark on tcp port
+80, you can set ambient capability `CAP_NET_BIND_SERVICE` from root,
+cd into www-root, setuid into unpriviliged user and exec into quark.
+
+quark already normalizes URL paths for ".." directories, thus internal
+path that is passed to filesystem-operating functions can safely be
+constructed with "./" or vhost prefix.
+
+The only way to leave the working directory is through symbolic links,
+but quark doesn't create any new files, so it's safe. Besides, symbolic
+links are usefull for pointing to the files outside of www-root without
+a need to bind-mount other directories, hardlinks or copy all the
+files. chroot(2) limits you from doing all that, besides creating more
+problems from changing process root directory.
This patch has not been tested with a UNIX-domain socket file.
Download
--------
* [quark-noroot-20191003-3c7049e.diff](quark-noroot-20191003-3c7049e.diff)
+* [quark-noroot-20260211-5ad0df9.diff](quark-noroot-20260211-5ad0df9.diff)
Author
------
* Richard Ulmer <codesoap AT mailbox DOT org>
+* Rogo <goryachev.romochka@gmail.com>
diff --git a/tools.suckless.org/quark/patches/noroot/quark-noroot-20260211-5ad0df9.diff b/tools.suckless.org/quark/patches/noroot/quark-noroot-20260211-5ad0df9.diff
@@ -0,0 +1,394 @@
+From 8d41e52f7a22b13008e111dbd5edbfc190f253ad Mon Sep 17 00:00:00 2001
+From: Rogo <goryachev.romochka@gmail.com>
+Date: Wed, 11 Feb 2026 02:31:33 +0300
+Subject: [PATCH 1/4] applying quark-noroot-20191003-3c7049e.diff
+
+---
+ main.c | 92 ++-------------------------------------------------------
+ quark.1 | 21 +------------
+ sock.c | 15 ++--------
+ sock.h | 2 +-
+ 4 files changed, 7 insertions(+), 123 deletions(-)
+
+diff --git a/main.c b/main.c
+index 1e88536..d4969fb 100644
+--- a/main.c
++++ b/main.c
+@@ -1,8 +1,7 @@
+ /* See LICENSE file for copyright and license details. */
+ #include <errno.h>
+-#include <grp.h>
+ #include <limits.h>
+-#include <pwd.h>
++#include <netinet/in.h>
+ #include <regex.h>
+ #include <signal.h>
+ #include <stddef.h>
+@@ -54,7 +53,7 @@ handlesignals(void(*hdl)(int))
+ static void
+ usage(void)
+ {
+- const char *opts = "[-u user] [-g group] [-n num] [-d dir] [-l] "
++ const char *opts = "[-n num] [-d dir] [-l] "
+ "[-i file] [-v vhost] ... [-m map] ...";
+
+ die("usage: %s -p port [-h host] %s\n"
+@@ -65,9 +64,6 @@ usage(void)
+ int
+ main(int argc, char *argv[])
+ {
+- struct group *grp = NULL;
+- struct passwd *pwd = NULL;
+- struct rlimit rlim;
+ struct server srv = {
+ .docindex = "index.html",
+ };
+@@ -80,16 +76,11 @@ main(int argc, char *argv[])
+ size_t nthreads = 4;
+ size_t nslots = 64;
+ char *servedir = ".";
+- char *user = "nobody";
+- char *group = "nogroup";
+
+ ARGBEGIN {
+ case 'd':
+ servedir = EARGF(usage());
+ break;
+- case 'g':
+- group = EARGF(usage());
+- break;
+ case 'h':
+ srv.host = EARGF(usage());
+ break;
+@@ -134,9 +125,6 @@ main(int argc, char *argv[])
+ case 'U':
+ udsname = EARGF(usage());
+ break;
+- case 'u':
+- user = EARGF(usage());
+- break;
+ case 'v':
+ if (spacetok(EARGF(usage()), tok, 4) || !tok[0] || !tok[1] ||
+ !tok[2]) {
+@@ -178,42 +166,10 @@ main(int argc, char *argv[])
+ }
+ }
+
+- /* validate user and group */
+- errno = 0;
+- if (!user || !(pwd = getpwnam(user))) {
+- die("getpwnam '%s': %s", user ? user : "null",
+- errno ? strerror(errno) : "Entry not found");
+- }
+- errno = 0;
+- if (!group || !(grp = getgrnam(group))) {
+- die("getgrnam '%s': %s", group ? group : "null",
+- errno ? strerror(errno) : "Entry not found");
+- }
+-
+ /* open a new process group */
+ setpgid(0, 0);
+
+ handlesignals(sigcleanup);
+-
+- /*
+- * set the maximum number of open file descriptors as needed
+- * - 3 initial fd's
+- * - nthreads fd's for the listening socket
+- * - (nthreads * nslots) fd's for the connection-fd
+- * - (5 * nthreads) fd's for general purpose thread-use
+- */
+- rlim.rlim_cur = rlim.rlim_max = 3 + nthreads + nthreads * nslots +
+- 5 * nthreads;
+- if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) {
+- if (errno == EPERM) {
+- die("You need to run as root or have "
+- "CAP_SYS_RESOURCE set, or are asking for more "
+- "file descriptors than the system can offer");
+- } else {
+- die("setrlimit:");
+- }
+- }
+-
+ /*
+ * create the (non-blocking) listening socket
+ *
+@@ -228,7 +184,7 @@ main(int argc, char *argv[])
+ * kernel by changing epoll-sheduling from a FIFO- to a
+ * LIFO-model, especially as it doesn't affect performance
+ */
+- insock = udsname ? sock_get_uds(udsname, pwd->pw_uid, grp->gr_gid) :
++ insock = udsname ? sock_get_uds(udsname) :
+ sock_get_ips(srv.host, srv.port);
+ if (sock_set_nonblocking(insock)) {
+ return 1;
+@@ -287,51 +243,9 @@ main(int argc, char *argv[])
+ eunveil(servedir, "r");
+ eunveil(NULL, NULL);
+
+- /* chroot */
+ if (chdir(servedir) < 0) {
+ die("chdir '%s':", servedir);
+ }
+- if (chroot(".") < 0) {
+- if (errno == EPERM) {
+- die("You need to run as root or have "
+- "CAP_SYS_CHROOT set");
+- } else {
+- die("chroot:");
+- }
+- }
+-
+- /* drop root */
+- if (pwd->pw_uid == 0 || grp->gr_gid == 0) {
+- die("Won't run under root %s for hopefully obvious reasons",
+- (pwd->pw_uid == 0) ? (grp->gr_gid == 0) ?
+- "user and group" : "user" : "group");
+- }
+-
+- if (setgroups(1, &(grp->gr_gid)) < 0) {
+- if (errno == EPERM) {
+- die("You need to run as root or have "
+- "CAP_SETGID set");
+- } else {
+- die("setgroups:");
+- }
+- }
+- if (setgid(grp->gr_gid) < 0) {
+- if (errno == EPERM) {
+- die("You need to run as root or have "
+- "CAP_SETGID set");
+- } else {
+- die("setgid:");
+- }
+-
+- }
+- if (setuid(pwd->pw_uid) < 0) {
+- if (errno == EPERM) {
+- die("You need to run as root or have "
+- "CAP_SETUID set");
+- } else {
+- die("setuid:");
+- }
+- }
+
+ if (udsname) {
+ epledge("stdio rpath proc unix", NULL);
+diff --git a/quark.1 b/quark.1
+index d752cc7..93126b5 100644
+--- a/quark.1
++++ b/quark.1
+@@ -46,13 +46,8 @@ hidden files and directories.
+ .It Fl d Ar dir
+ Serve
+ .Ar dir
+-after chrooting into it.
++after changing into it.
+ The default is ".".
+-.It Fl g Ar group
+-Set group ID when dropping privileges, and in socket mode the group of the
+-socket file, to the ID of
+-.Ar group .
+-The default is "nogroup".
+ .It Fl h Ar host
+ Use
+ .Ar host
+@@ -94,20 +89,6 @@ redirects on non-standard ports.
+ Create the UNIX-domain socket
+ .Ar file ,
+ listen on it for incoming connections and remove it on exit.
+-.It Fl s Ar num
+-Set the number of connection slots per worker thread to
+-.Ar num .
+-The default is 64.
+-.It Fl t Ar num
+-Set the number of worker threads to
+-.Ar num .
+-The default is 4.
+-.It Fl u Ar user
+-Set user ID when dropping privileges,
+-and in socket mode the user of the socket file,
+-to the ID of
+-.Ar user .
+-The default is "nobody".
+ .It Fl v Ar vhost
+ Add the virtual host specified by
+ .Ar vhost ,
+diff --git a/sock.c b/sock.c
+index ebbbf65..bff1b38 100644
+--- a/sock.c
++++ b/sock.c
+@@ -70,14 +70,13 @@ sock_get_ips(const char *host, const char* port)
+ }
+
+ int
+-sock_get_uds(const char *udsname, uid_t uid, gid_t gid)
++sock_get_uds(const char *udsname)
+ {
+ struct sockaddr_un addr = {
+ .sun_family = AF_UNIX,
+ };
+ size_t udsnamelen;
+- int insock, sockmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
+- S_IROTH | S_IWOTH;
++ int insock;
+
+ if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
+ die("socket:");
+@@ -97,16 +96,6 @@ sock_get_uds(const char *udsname, uid_t uid, gid_t gid)
+ die("listen:");
+ }
+
+- if (chmod(udsname, sockmode) < 0) {
+- sock_rem_uds(udsname);
+- die("chmod '%s':", udsname);
+- }
+-
+- if (chown(udsname, uid, gid) < 0) {
+- sock_rem_uds(udsname);
+- die("chown '%s':", udsname);
+- }
+-
+ return insock;
+ }
+
+diff --git a/sock.h b/sock.h
+index 22ae303..765016d 100644
+--- a/sock.h
++++ b/sock.h
+@@ -7,8 +7,8 @@
+ #include <sys/types.h>
+
+ int sock_get_ips(const char *, const char *);
+-int sock_get_uds(const char *, uid_t, gid_t);
+ void sock_rem_uds(const char *);
++int sock_get_uds(const char *);
+ int sock_set_timeout(int, int);
+ int sock_set_nonblocking(int);
+ int sock_get_inaddr_str(const struct sockaddr_storage *, char *, size_t);
+--
+2.52.0
+
+
+From 56bde27402884fd051cbe9e5def222b1d4fb5fdb Mon Sep 17 00:00:00 2001
+From: Rogo <goryachev.romochka@gmail.com>
+Date: Wed, 11 Feb 2026 02:44:40 +0300
+Subject: [PATCH 2/4] Remove setrlimit() code
+
+User other tools to setup process limits, capabilities, credentials.
+---
+ main.c | 28 ----------------------------
+ 1 file changed, 28 deletions(-)
+
+diff --git a/main.c b/main.c
+index d4969fb..8bb7023 100644
+--- a/main.c
++++ b/main.c
+@@ -211,34 +211,6 @@ main(int argc, char *argv[])
+ die("signal: Failed to set SIG_IGN on SIGPIPE");
+ }
+
+- /*
+- * try increasing the thread-limit by the number
+- * of threads we need (which is the only reliable
+- * workaround I know given the thread-limit is per user
+- * rather than per process), but ignore EPERM errors,
+- * because this most probably means the user has already
+- * set the value to the kernel's limit, and there's not
+- * much we can do in any other case.
+- * There's also no danger of overflow as the value
+- * returned by getrlimit() is way below the limits of the
+- * rlim_t datatype.
+- */
+- if (getrlimit(RLIMIT_NPROC, &rlim) < 0) {
+- die("getrlimit:");
+- }
+- if (rlim.rlim_max == RLIM_INFINITY) {
+- if (rlim.rlim_cur != RLIM_INFINITY) {
+- /* try increasing current limit by nthreads */
+- rlim.rlim_cur += nthreads;
+- }
+- } else {
+- /* try increasing current and hard limit by nthreads */
+- rlim.rlim_cur = rlim.rlim_max += nthreads;
+- }
+- if (setrlimit(RLIMIT_NPROC, &rlim) < 0 && errno != EPERM) {
+- die("setrlimit()");
+- }
+-
+ /* limit ourselves to reading the servedir and block further unveils */
+ eunveil(servedir, "r");
+ eunveil(NULL, NULL);
+--
+2.52.0
+
+
+From a31e262d003b0ffdc4bdeff8d6690cd11d5e2f45 Mon Sep 17 00:00:00 2001
+From: Rogo <goryachev.romochka@gmail.com>
+Date: Wed, 11 Feb 2026 07:13:49 +0300
+Subject: [PATCH 3/4] Make internal_path relative to "." or vhost dir
+
+Absolute pathnames for filesystem paths only work with chroot.
+
+Without even dealing with other problems that abuse of chroot for
+"sandboxing" introduces, it makes impossible to run quark as unpriviliged
+user. URL path is already sanitized for ".." dirs, and quark doesn't
+create new files and links.
+---
+ http.c | 8 ++------
+ 1 file changed, 2 insertions(+), 6 deletions(-)
+
+diff --git a/http.c b/http.c
+index 36f8b1c..9d17cfb 100644
+--- a/http.c
++++ b/http.c
+@@ -776,16 +776,12 @@ http_prepare_response(const struct request *req, struct response *res,
+ * path and the virtual host while ignoring query and fragment
+ * (valid according to RFC 3986)
+ */
+- if (esnprintf(res->internal_path, sizeof(res->internal_path), "/%s/%s",
+- (srv->vhost && res->vhost) ? res->vhost->dir : "",
++ if (esnprintf(res->internal_path, sizeof(res->internal_path), "%s/%s",
++ (srv->vhost && res->vhost) ? res->vhost->dir : ".",
+ res->path)) {
+ s = S_REQUEST_TOO_LARGE;
+ goto err;
+ }
+- if ((tmps = path_normalize(res->internal_path, NULL))) {
+- s = tmps;
+- goto err;
+- }
+ if (stat(res->internal_path, &st) < 0) {
+ s = (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND;
+ goto err;
+--
+2.52.0
+
+
+From c67c92dcbdc00f5a16179da8d46525c70a28d219 Mon Sep 17 00:00:00 2001
+From: Rogo <goryachev.romochka@gmail.com>
+Date: Wed, 11 Feb 2026 07:25:23 +0300
+Subject: [PATCH 4/4] Remove "setpgid(0, 0)"; stop breaking ^C behavior in
+ shell
+
+---
+ main.c | 3 ---
+ 1 file changed, 3 deletions(-)
+
+diff --git a/main.c b/main.c
+index 8bb7023..776fb13 100644
+--- a/main.c
++++ b/main.c
+@@ -166,9 +166,6 @@ main(int argc, char *argv[])
+ }
+ }
+
+- /* open a new process group */
+- setpgid(0, 0);
+-
+ handlesignals(sigcleanup);
+ /*
+ * create the (non-blocking) listening socket
+--
+2.52.0
+