commit ea81780

Alvar Penning  ·  2026-02-05 19:58:25 +0000 UTC
parent 81e4613
strip_exif support for the OpenBSD sandbox

Change the strip_exif logic to work with the already existing OpenBSD
sandbox and allow ffmpeg and mogrify to be executed.

The previous strip_exif implementation relied on system(3), effectively
starting "/bin/sh" and executing the required tool within a shell
session. Making this work in the sandbox would require to allow
executing "/bin/sh", rendering the sandbox useless.

Thus, the code now starts determining the absolute path of the tools -
unless they are given as ffmpeg_path or mogrify_path - and allowing them
to be executed via unveil(2). Then, instead of the system(3) call, the
good old fork(2) and execve(2) dance is performed.

The sbox_enter code was made aware of strip_exif, which resulted in a
pledge(2) violation before when disable_email_notifications was set to
false. Furthermore, the detected paths of the tools are now allowed.
4 files changed,  +114, -73
M data.c
M snac.c
M snac.h
M data.c
+6, -13
 1@@ -100,19 +100,12 @@ int srv_open(const char *basedir, int auto_upgrade)
 2 
 3                 if (auto_upgrade)
 4                     ret = snac_upgrade(&error);
 5-                else {
 6-                    if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout)
 7-                        error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first");
 8-                    else {
 9-                        if (!check_strip_tool()) {
10-                            const char *mp = xs_dict_get(srv_config, "mogrify_path");
11-                            if (mp == NULL) mp = "mogrify";
12-                            error = xs_fmt("ERROR: strip_exif enabled but '%s' not found or not working (set 'mogrify_path' in server.json)", mp);
13-                        }
14-                        else
15-                            ret = 1;
16-                    }
17-                }
18+                else if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout)
19+                    error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first");
20+                else if (!check_strip_tool())
21+                    error = xs_fmt("ERROR: strip_exif enabled but commands not found or working");
22+                else
23+                    ret = 1;
24             }
25 
26         }
+8, -1
 1@@ -13,6 +13,8 @@ void sbox_enter(const char *basedir)
 2         return;
 3     }
 4 
 5+    const xs_val *strip_exif = xs_dict_get(srv_config, "strip_exif");
 6+
 7     int smail;
 8     const char *url = xs_dict_get(srv_config, "smtp_url");
 9 
10@@ -33,6 +35,11 @@ void sbox_enter(const char *basedir)
11     if (*address == '/')
12         unveil(address, "rwc");
13 
14+    if (strip_exif) {
15+        unveil(xs_dict_get(srv_config, "ffmpeg_path"), "x");
16+        unveil(xs_dict_get(srv_config, "mogrify_path"), "x");
17+    }
18+
19     if (smail)
20         unveil("/usr/sbin/sendmail",   "x");
21 
22@@ -45,7 +52,7 @@ void sbox_enter(const char *basedir)
23     if (*address == '/')
24         p = xs_str_cat(p, " unix");
25 
26-    if (smail)
27+    if (smail || strip_exif)
28         p = xs_str_cat(p, " exec");
29 
30     pledge(p, NULL);
M snac.c
+99, -59
  1@@ -35,7 +35,9 @@
  2 #include <sys/stat.h>
  3 #include <sys/wait.h>
  4 #include <limits.h>
  5+#include <stdio.h>
  6 #include <stdlib.h>
  7+#include <unistd.h>
  8 
  9 xs_str *srv_basedir = NULL;
 10 xs_dict *srv_config = NULL;
 11@@ -177,6 +179,46 @@ int check_password(const char *uid, const char *passwd, const char *hash)
 12 }
 13 
 14 
 15+char* findprog(const char *prog)
 16+/* find a prog in PATH and return the first match */
 17+{
 18+    char *path_env, *path, *dir, filename[PATH_MAX];
 19+    int len;
 20+    struct stat sbuf;
 21+
 22+    path_env = getenv("PATH");
 23+    if (!prog || !path_env)
 24+        return NULL;
 25+
 26+    path_env = strdup(path_env);
 27+    if (!path_env)
 28+        return NULL;
 29+    path = path_env;
 30+
 31+    while ((dir = strsep(&path, ":")) != NULL) {
 32+        /* empty entries as ./ instead of / */
 33+        if (*dir == '\0')
 34+            dir = ".";
 35+
 36+        /* strip trailing / */
 37+        len = strlen(dir);
 38+        while (len > 0 && dir[len-1] == '/')
 39+            dir[--len] = '\0';
 40+
 41+        len = snprintf(filename, sizeof(filename), "%s/%s", dir, prog);
 42+        if (len > 0 && len < (int) sizeof(filename) &&
 43+            (stat(filename, &sbuf) == 0) && S_ISREG(sbuf.st_mode) &&
 44+            access(filename, X_OK) == 0) {
 45+            free(path_env);
 46+            return strdup(filename);
 47+        }
 48+    }
 49+
 50+    free(path_env);
 51+    return NULL;
 52+}
 53+
 54+
 55 int strip_media(const char *fn)
 56 /* strips EXIF data from a file */
 57 {
 58@@ -207,23 +249,24 @@ int strip_media(const char *fn)
 59             xs_endswith(l_fn, ".gif")  || xs_endswith(l_fn, ".bmp")) {
 60 
 61             const char *mp = xs_dict_get(srv_config, "mogrify_path");
 62-            if (mp == NULL)
 63-                mp = "mogrify";
 64 
 65-            xs *cmd = xs_fmt("cd \"%s\" && %s -auto-orient -strip \"%s\" 2>/dev/null", srv_basedir, mp, r_fn);
 66-
 67-            ret = system(cmd);
 68+            pid_t pid = fork();
 69+            if (pid == -1) {
 70+                srv_log(xs_fmt("strip_media: cannot fork()"));
 71+                return -1;
 72+            } else if (pid == 0) {
 73+                chdir(srv_basedir);
 74+                execl(mp, "-auto-orient", "-strip", r_fn, (char*) NULL);
 75+                _exit(1);
 76+            }
 77 
 78-            if (ret != 0) {
 79-                int code = 0;
 80-                if (WIFEXITED(ret))
 81-                    code = WEXITSTATUS(ret);
 82-                
 83-                if (code == 127)
 84-                    srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'mogrify_path' in server.json.", r_fn, mp));
 85-                else
 86-                    srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
 87+            if (waitpid(pid, &ret, 0) == -1) {
 88+                srv_log(xs_fmt("strip_media: cannot waitpid()"));
 89+                return -1;
 90             }
 91+
 92+            if (ret != 0)
 93+                srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
 94             else
 95                 srv_debug(1, xs_fmt("strip_media: stripped %s", r_fn));
 96         }
 97@@ -234,35 +277,33 @@ int strip_media(const char *fn)
 98             xs_endswith(l_fn, ".mkv") || xs_endswith(l_fn, ".avi")) {
 99 
100             const char *fp = xs_dict_get(srv_config, "ffmpeg_path");
101-            if (fp == NULL)
102-                fp = "ffmpeg";
103 
104             /* ffmpeg cannot modify in-place, so we need a temp file */
105             /* we must preserve valid extension for ffmpeg to guess the format */
106             const char *ext = strrchr(r_fn, '.');
107             if (ext == NULL) ext = "";
108             xs *tmp_fn = xs_fmt("%s.tmp%s", r_fn, ext);
109-            
110-            /* -map_metadata -1 strips all global metadata */
111-            /* -c copy copies input streams without re-encoding */
112-            /* we don't silence stderr so we can debug issues */
113-            /* we explicitly cd to srv_basedir to ensure relative paths work */
114-            xs *cmd = xs_fmt("cd \"%s\" && %s -y -i \"%s\" -map_metadata -1 -c copy \"%s\"", srv_basedir, fp, r_fn, tmp_fn);
115 
116-            ret = system(cmd);
117+            pid_t pid = fork();
118+            if (pid == -1) {
119+                srv_log(xs_fmt("strip_media: cannot fork()"));
120+                return -1;
121+            } else if (pid == 0) {
122+                chdir(srv_basedir);
123+                /* -map_metadata -1 strips all global metadata */
124+                /* -c copy copies input streams without re-encoding */
125+                execl(fp, "-y", "-i", r_fn, "-map_metadata", "-1", "-c", "copy", tmp_fn, (char*) NULL);
126+                _exit(1);
127+            }
128+
129+            if (waitpid(pid, &ret, 0) == -1) {
130+                srv_log(xs_fmt("strip_media: cannot waitpid()"));
131+                return -1;
132+            }
133 
134             if (ret != 0) {
135-                int code = 0;
136-                if (WIFEXITED(ret))
137-                    code = WEXITSTATUS(ret);
138-                
139-                if (code == 127)
140-                    srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'ffmpeg_path' in server.json.", r_fn, fp));
141-                else {
142-                    srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
143-                    srv_log(xs_fmt("strip_media: command was: %s", cmd));
144-                }
145-                
146+                srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
147+
148                 /* try to cleanup, just in case */
149                 /* unlink needs full path too if we are not in basedir */
150                 xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn);
151@@ -286,38 +327,37 @@ int strip_media(const char *fn)
152 
153 
154 int check_strip_tool(void)
155+/* check if strip_exif tools do exist and fix their absolute path */
156 {
157     const xs_val *v = xs_dict_get(srv_config, "strip_exif");
158+    /* skip if unless strip_exif; return non-error */
159+    if (xs_type(v) != XSTYPE_TRUE)
160+        return 1;
161+
162     int ret = 1;
163+    const char *progs[] = { "ffmpeg", "mogrify" };
164 
165-    if (xs_type(v) == XSTYPE_TRUE) {
166-        /* check mogrify */
167-        {
168-            const char *mp = xs_dict_get(srv_config, "mogrify_path");
169-            if (mp == NULL)
170-                mp = "mogrify";
171-
172-            xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", mp);
173-            
174-            if (system(cmd) != 0) {
175-                srv_log(xs_fmt("check_strip_tool: '%s' not working", mp));
176-                ret = 0;
177-            }
178+    for (int i = 0; i < (int)(sizeof(progs) / sizeof(progs[0])); i++) {
179+        xs_str *key = xs_fmt("%s_path", progs[i]);
180+
181+        const char *val = xs_dict_get(srv_config, key);
182+        if (val == NULL) {
183+            val = findprog(progs[i]);
184+            if (val != NULL)
185+                srv_debug(1, xs_fmt("check_strip_tool: found %s in PATH at %s", progs[i], val));
186         }
187 
188-        /* check ffmpeg */
189-        if (ret) {
190-            const char *fp = xs_dict_get(srv_config, "ffmpeg_path");
191-            if (fp == NULL)
192-                fp = "ffmpeg";
193-
194-            xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", fp);
195-            
196-            if (system(cmd) != 0) {
197-                srv_log(xs_fmt("check_strip_tool: '%s' not working", fp));
198-                ret = 0;
199-            }
200+        if (val == NULL) {
201+            srv_log(xs_fmt("check_strip_tool: %s not found in PATH", progs[i]));
202+            ret = 0;
203+        } else if (access(val, X_OK) != 0) {
204+            srv_log(xs_fmt("check_strip_tool: %s '%s' is not executable", progs[i], val));
205+            ret = 0;
206+        } else {
207+            srv_config = xs_dict_set(srv_config, key, val);
208         }
209+
210+        xs_free(key);
211     }
212 
213     return ret;
M snac.h
+1, -0
1@@ -107,6 +107,7 @@ int validate_uid(const char *uid);
2 xs_str *hash_password(const char *uid, const char *passwd, const char *nonce);
3 int check_password(const char *uid, const char *passwd, const char *hash);
4 
5+char* findprog(const char *prog);
6 int strip_media(const char *fn);
7 int check_strip_tool(void);
8