Initial import

This commit is contained in:
Aad Versteden
2026-09-22 10:55:10 +02:00
commit a9349d3107
6 changed files with 330 additions and 0 deletions

17
Makefile Normal file
View File

@@ -0,0 +1,17 @@
all:
cc -O2 -Wall -Wextra -DSTAGING_ROOT=\"$(HOME)/opencode-workspace/workspace\" -o src/mountctl src/mountctl.c
install: all
mkdir -p workspace
touch mounts.list
ln -sf src/mount-opencode mount-opencode
ln -sf src/unmount-opencode unmount-opencode
ln -sf src/recover-mounts recover-mounts
@echo
@echo "Now make the helper setuid root:"
@echo " sudo chown root $(CURDIR)/src/mountctl && sudo chmod 4755 $(CURDIR)/src/mountctl"
clean:
rm -f src/mountctl
.PHONY: all install clean

133
README.org Normal file
View File

@@ -0,0 +1,133 @@
#+TITLE: OpenCode live mounts
#+STARTUP: :indent
Bind-mount host folders into a *running* Docker container, add and remove
them at will, and re-apply them after a reboot. No daemon, no Docker API
tricks — one setuid helper and three shell scripts.
* Tutorial: get started
Build and install the helper and scripts:
#+begin_src bash
make
make install
sudo chown root ~/opencode-workspace/src/mountctl
sudo chmod 4755 ~/opencode-workspace/src/mountctl
#+end_src
Give the container a window into the staging area. Add this =volumes=
block to your service (replace =source= with the absolute path to your
staging directory) and bring it up once:
#+begin_src yaml
services:
app:
image: ubuntu
volumes:
- type: bind
source: /home/you/opencode-workspace/workspace
target: /workspace
bind:
propagation: rslave
entrypoint: "tail -f /dev/null"
#+end_src
#+begin_src bash
docker compose up -d
#+end_src
Now mount something:
#+begin_src bash
~/opencode-workspace/mount-opencode /home/you/code/elm
#+end_src
It shows up at =/workspace/elm= inside the running container. That's it.
* How-to guides
** Add a mount
#+begin_src bash
~/opencode-workspace/mount-opencode <source> [name]
#+end_src
=name= defaults to =basename <source>=. The mount is recorded in
=mounts.list=.
** Remove a mount
#+begin_src bash
~/opencode-workspace/unmount-opencode <name>
#+end_src
** Recover all mounts after a reboot
Bring the container back up first (its rslave bind must be in place),
then:
#+begin_src bash
~/opencode-workspace/recover-mounts
#+end_src
It re-runs every mount listed in =mounts.list=; safe to run anytime.
* Explanation
Docker has no way to add a mount to a running container, and =nsenter=
mounts can't see host paths that aren't already bind-mounted in. The
=rslave= propagation on the staging directory is the bridge: it is the
one path shared between host and container namespaces, and any sub-mount
created *on the host* under it propagates into the container. =rslave=
is one-way (host → container), so container-side mounts don't leak back
onto the host.
The staging directory must therefore exist before the container starts,
and the container must be recreated (a brief restart) if it was started
without the bind. This is a Docker limitation, not a tool choice.
Why setuid root: =mount(2)= requires =CAP_SYS_ADMIN=, which ordinary
users don't have. A compiled C helper is the only thing Linux honours
setuid on (scripts are ignored). The helper does *only* raw bind mounts
and umounts — no shell, no =system()= — and rejects names that could
escape the staging root (= /=, leading dot, empty) or that point at an
existing non-directory (symlink-escape guard).
* Reference
** =mountctl= (the setuid helper)
#+begin_example
mountctl mount <source> <name> # bind-mount source at STAGING_ROOT/name
mountctl umount <name> # umount and rmdir the mountpoint
#+end_example
=STAGING_ROOT= is baked in at compile time; default is
=$(HOME)/opencode-workspace/workspace=. Override at build time:
#+begin_src bash
make STAGING_ROOT=/some/other/path
#+end_src
** =mounts.list=
Plain text, one mount per line, tab-separated:
#+begin_example
<source>\t<name>
#+end_example
Managed by the scripts; edit by hand if you must.
** Makefile targets
| =make= | compile =src/mountctl= |
| =make install= | deploy to =~/opencode-workspace/= and create the symlinks |
| =make clean= | remove the binary |
** Authorization note
The helper is setuid root (mode 4755) for now. TODO: restrict to a
=live-mount= Unix group via =getgroups(2)=. If your home filesystem is
mounted =nosuid= the setuid bit is silently ignored — check with
=mount | grep 'on /home'=.

23
src/mount-opencode Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Mount a host folder into the staging area so it appears inside the
# running container (which bind-mounts the staging area with rslave).
# Records the mount in mounts.list so recover-mounts can redo it.
set -euo pipefail
here="$(cd "$(dirname "$(realpath "$0")")" && pwd)" # .../src
root="$(dirname "$here")" # .../opencode-workspace
staging="$root/workspace"
record="$root/mounts.list"
mountctl="$here/mountctl"
if [ "$#" -lt 1 ]; then
echo "usage: mount-opencode <source> [name]" >&2
exit 2
fi
src="$(realpath "$1")"
name="${2:-$(basename "$src")}"
mkdir -p "$staging/$name"
"$mountctl" mount "$src" "$name"
printf '%s\t%s\n' "$src" "$name" >> "$record"

120
src/mountctl.c Normal file
View File

@@ -0,0 +1,120 @@
#define _GNU_SOURCE
#include <sys/mount.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#ifndef STAGING_ROOT
#error "compile with -DSTAGING_ROOT=..."
#endif
/* Authorization: setuid root for now. TODO: restrict to a "live-mount"
Unix group via getgroups(2) so only members may run this binary. */
/* The name is a relative path under STAGING_ROOT: non-empty components,
each not starting with '.' and surrounded by '/'. */
static int valid_name(const char *name) {
const char *p = name;
if (!p || !*p || *p == '/' || *p == '.') {
return 0;
} else {
while (*p) {
if (*p == '.') {
return 0;
} else {
while (*p && *p != '/') {
p++;
}
if (*p == '/') {
if (p[1] == '/' || p[1] == '.' || !p[1]) {
return 0;
} else {
p++;
}
} else {
return 1;
}
}
}
return 1;
}
}
static char *target_for(const char *name) {
char *t = malloc(strlen(STAGING_ROOT) + 1 + strlen(name) + 1);
if (t) {
sprintf(t, "%s/%s", STAGING_ROOT, name);
} else {
fprintf(stderr, "out of memory\n");
}
return t;
}
static int do_mount(const char *source, const char *name) {
if (valid_name(name)) {
char *target = target_for(name);
if (target) {
if (mount(source, target, NULL, MS_BIND, NULL) == 0) {
printf("%s -> %s\n", source, target);
free(target);
return 0;
} else {
perror("mount");
free(target);
return 1;
}
} else {
return 1;
}
} else {
fprintf(stderr, "bad name: %s\n", name ? name : "(null)");
return 1;
}
}
static int do_umount(const char *name) {
if (valid_name(name)) {
char *target = target_for(name);
if (target) {
if (umount2(target, 0) == 0) {
printf("unmounted %s\n", target);
rmdir(target);
free(target);
return 0;
} else {
perror("umount");
free(target);
return 1;
}
} else {
return 1;
}
} else {
fprintf(stderr, "bad name: %s\n", name ? name : "(null)");
return 1;
}
}
int main(int argc, char **argv) {
if (argc > 1 && strcmp(argv[1], "mount") == 0) {
if (argc == 4) {
return do_mount(argv[2], argv[3]);
} else {
fprintf(stderr, "usage: mountctl mount <source> <name>\n");
return 2;
}
} else if (argc > 1 && strcmp(argv[1], "umount") == 0) {
if (argc == 3) {
return do_umount(argv[2]);
} else {
fprintf(stderr, "usage: mountctl umount <name>\n");
return 2;
}
} else {
fprintf(stderr, "usage: mountctl mount <source> <name>\n");
fprintf(stderr, " mountctl umount <name>\n");
return 2;
}
}

18
src/recover-mounts Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Re-apply every mount recorded in mounts.list. Run this after a reboot,
# once the container (with its rslave bind) is back up.
set -euo pipefail
here="$(cd "$(dirname "$(realpath "$0")")" && pwd)"
root="$(dirname "$here")"
staging="$root/workspace"
record="$root/mounts.list"
mountctl="$here/mountctl"
mkdir -p "$staging"
[ -f "$record" ] || exit 0
while IFS=$'\t' read -r src name; do
[ -z "${name:-}" ] && continue
mkdir -p "$staging/$name"
"$mountctl" mount "$src" "$name"
done < "$record"

19
src/unmount-opencode Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Unmount a name from the staging area and drop it from mounts.list.
set -euo pipefail
here="$(cd "$(dirname "$(realpath "$0")")" && pwd)"
root="$(dirname "$here")"
record="$root/mounts.list"
mountctl="$here/mountctl"
if [ "$#" -ne 1 ]; then
echo "usage: unmount-opencode <name>" >&2
exit 2
fi
name="$1"
"$mountctl" umount "$name"
tmp="$(mktemp)"
awk -F'\t' -v n="$name" '$2 != n' "$record" > "$tmp"
mv "$tmp" "$record"