{"id":17526245,"url":"https://github.com/msantos/alcove","last_synced_at":"2025-04-15T01:29:22.080Z","repository":{"id":14621541,"uuid":"17338903","full_name":"msantos/alcove","owner":"msantos","description":"Control plane for system processes","archived":false,"fork":false,"pushed_at":"2025-04-13T12:22:09.000Z","size":1674,"stargazers_count":49,"open_issues_count":1,"forks_count":2,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-13T13:27:09.988Z","etag":null,"topics":["capsicum","exec","fork","linux-namespaces","pledge","prctl","procctl","seccomp","signal","system-programming"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/msantos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2014-03-02T14:09:39.000Z","updated_at":"2025-04-13T12:22:12.000Z","dependencies_parsed_at":"2023-11-08T12:34:07.875Z","dependency_job_id":"b0a1416d-65f1-4de1-8f93-73c8f4c28d76","html_url":"https://github.com/msantos/alcove","commit_stats":{"total_commits":1062,"total_committers":2,"mean_commits":531.0,"dds":0.0009416195856873921,"last_synced_commit":"4f0a01dedba380b7c347caf568d78759c8b7d858"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Falcove","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Falcove/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Falcove/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Falcove/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/msantos","download_url":"https://codeload.github.com/msantos/alcove/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248721265,"owners_count":21151073,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["capsicum","exec","fork","linux-namespaces","pledge","prctl","procctl","seccomp","signal","system-programming"],"created_at":"2024-10-20T15:01:29.165Z","updated_at":"2025-04-15T01:29:22.072Z","avatar_url":"https://github.com/msantos.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"# alcove\n\n[![Package Version](https://img.shields.io/hexpm/v/alcove)](https://hex.pm/packages/alcove)\n[![Hex Docs](https://img.shields.io/badge/hex-docs)](https://hexdocs.pm/alcove/)\n\nalcove is:\n\n* a control plane for system processes\n* an interface for system programming\n* a library for building containerized services\n\n*alcove* is an external port process (a stand-alone\nUnix process that communicates with the Erlang VM using\nstdin/stdout). [prx](https://github.com/msantos/prx) is a higher level\nlibrary that maps the alcove Unix processes to Erlang processes.\n\n## Build\n\n```\nrebar3 compile\n\n# to run tests (see \"Setting Up Privileges\")\nrebar3 do clean, compile, ct\n\n# Linux: statically link using musl\nsudo apt install musl-dev musl-tools\n\n# clone the kernel headers somewhere\nexport MUSL_INCLUDE=/tmp\ngit clone https://github.com/sabotage-linux/kernel-headers.git $MUSL_INCLUDE/kernel-headers\n\n# then compile\n./musl-wrapper rebar3 do clean, compile\n\n## Generate code\nmake gen\n```\n\n## Overview\n\nWhen alcove is started, it enters an event loop:\n\n```erlang\n{ok, Drv} = alcove_drv:start().\n```\n\nSimilar to a shell, alcove waits for a command. For example, alcove can\nbe requested to fork(2):\n\n```erlang\n{ok, Child1} = alcove:fork(Drv, []).\n```\n\nNow there are 2 processes in a parent/child relationship, sitting in\ntheir event loops:\n\n```\nbeam.smp\n  |-erl_child_setup\n  |   `-alcove\n  |       `-alcove\n```\n\nProcesses are arranged in a pipeline:\n\n* a pipeline is a list of 0 or more integers representing the process IDs\n\n  By default, pipelines are limited to a length of 16 processes. The\n  pipeline length can be increased using getopt/3 up to the system limits.\n\n* unlike in a shell, each successive process in the pipeline is forked\n  from the previous process\n\n* like a shell pipeline, the stdout of a process is connected to the\n  stdin of the next process in the pipeline using a FIFO\n\nThe child process is addressed via the pipeline using a list of PIDs:\n\n```erlang\n{ok, Child2} = alcove:fork(Drv, [Child1]),\nChild2 = alcove:getpid(Drv, [Child1, Child2]).\n```\n\nAn empty pipeline refers to the port process:\n\n```erlang\n{ok, Child3} = alcove:fork(Drv, []).\n```\n\nFinally, we can replace the event loop with a system executable by\ncalling exec(3):\n\n```erlang\nok = alcove:execvp(Drv, [Child1, Child2], \"/bin/cat\", [\"/bin/cat\"]).\n```\n\nThe process tree now looks like:\n\n```\nbeam.smp\n  |-erl_child_setup\n  |   `-alcove\n  |       |-alcove\n  |       |   `-cat\n  |       `-alcove\n```\n\nWe can interact with the process via stdin, stdout and stderr:\n\n```erlang\nalcove:stdin(Drv, [Child1, Child2], \"hello process\\n\"),\n[\u003c\u003c\"hello process\\n\"\u003e\u003e] = alcove:stdout(Drv, [Child1, Child2]).\n```\n\n## Setting Up Privileges\n\n* sudo\n\n```\nsudo visudo -f /etc/sudoers.d/99_alcove\n\u003cuser\u003e ALL = NOPASSWD: /path/to/alcove/priv/alcove\nDefaults!/path/to/alcove/priv/alcove !requiretty\n```\n\nWhen starting alcove, pass in the `exec` option:\n\n```erlang\n{ok, Drv} = alcove_drv:start([{exec, \"sudo -n\"}]).\n```\n\n* setuid\n\n```\nchown root:root priv/alcove\nchmod u+s priv/alcove\n```\n\n* Linux: file capabilities\n\n  See capabilities(7) and setcap(8).\n\n## Creating a chroot\n\nThe standard Unix way of sandboxing a process is by doing a chroot(2). The\nchroot process involves:\n\n* running as root\n* setting process limits\n* changing the root directory to limit the process view of the filesystem\n* changing to an unprivileged user\n* running the sandboxed code\n\nSee `examples/chrootex.erl`.\n\nWe'll create a chroot using an interface like:\n\n```erlang\n-spec sandbox(port(), [iodata()]) -\u003e non_neg_integer().\nsandbox(Drv, [\"/bin/sh\", \"-i\"]).\n```\n\nThe function returns the system PID of the child process. This would\ncreate an interactive shell we access through standard I/O.\n\nIn order to call chroot(2), the port will need root privileges:\n\n```erlang\n{ok, Drv} = alcove_drv:start([{exec, \"sudo -n\"}]).\n```\n\nFollowing the steps outlined earlier, we want to set some process\nlimits. In this case, we'll use setrlimit(2):\n\n```erlang\nsetlimits(Drv, Child) -\u003e\n    % Disable creation of files\n    ok = alcove:setrlimit(\n        Drv,\n        [Child],\n        rlimit_fsize,\n        #alcove_rlimit{cur = 0, max = 0}\n    ),\n\n    ok = alcove:setrlimit(\n        Drv,\n        [Child],\n        rlimit_nofile,\n        #alcove_rlimit{cur = 0, max = 0}\n    ),\n\n    % Limit to one process\n    ok = alcove:setrlimit(\n        Drv,\n        [Child],\n        rlimit_nproc,\n        #alcove_rlimit{cur = 1, max = 1}\n    ).\n```\n\nNext we chroot and drop root privileges. We will set the user and group\nto a random, high UID/GID that is unlikely to conflict with an existing\nsystem user:\n\n```erlang\nchroot(Drv, Child, Path) -\u003e\n    ok = alcove:chroot(Drv, [Child], Path),\n    ok = alcove:chdir(Drv, [Child], \"/\").\n\ndrop_privs(Drv, Child, Id) -\u003e\n    ok = alcove:setgid(Drv, [Child], Id),\n    ok = alcove:setuid(Drv, [Child], Id).\n\nid() -\u003e\n    16#f0000000 + crypto:rand_uniform(0, 16#ffff).\n```\n\nTying it all together:\n\n```erlang\n% The default is to run the cat command. Because of the chroot, we need\n% to use a statically linked executable.\nsandbox(Drv) -\u003e\n    sandbox(Drv, [\"/bin/busybox\", \"cat\"]).\nsandbox(Drv, Argv) -\u003e\n    {Path, Arg0, Args} = argv(Argv),\n\n    {ok, Child} = alcove:fork(Drv, []),\n\n    setlimits(Drv, Child),\n    chroot(Drv, Child, Path),\n    drop_privs(Drv, Child, id()),\n\n    ok = alcove:execvp(Drv, [Child], Arg0, [Arg0, Args]),\n\n    Child.\n\n% Set the program path for the chroot\nargv([Arg0, Args]) -\u003e\n    Path = filename:dirname(Arg0),\n    Progname = filename:join([\"/\", filename:basename(Arg0)]),\n    {Path, Progname, Args}.\n```\n\nCompile and run the example:\n\n```\n# If alcove is in ~/src/alcove\nexport ERL_LIBS=~/src\nmake eg\nrebar shell\n```\n\n```erlang\n1\u003e {ok, Drv} = chrootex:start().\n\n2\u003e Cat = chrootex:sandbox(Drv).\n31831\n\n3\u003e alcove:stdin(Drv, [Cat], \"test test\\n\").\n4\u003e alcove:stdout(Drv, [Cat]).\n[\u003c\u003c\"test test\\n\"\u003e\u003e]\n```\n\nWe can test the limits of the sandbox by using a shell instead of\nherding cats:\n\n```erlang\n5\u003e Sh = chrootex:sandbox(Drv, [\"/bin/busybox\", \"sh\"]).\n31861\n\n% Test the shell is working\n6\u003e alcove:stdin(P, [Sh], \"echo hello\\n\").\nok\n7\u003e alcove:stdout(P, [Sh]).\n[\u003c\u003c\"hello\\n\"\u003e\u003e]\n\n% Attempt to create a file\n6\u003e alcove:stdin(Drv, [Sh], \"\u003e foo\\n\").\nok\n7\u003e alcove:stderr(P, [Sh]).\n[\u003c\u003c\"sh: can't create foo: Too many open files\\n\"\u003e\u003e]\n\n% Try to fork a new process\n8\u003e alcove:stdin(Drv, [Sh], \"ls\\n\").\n9\u003e alcove:stderr(P, [Sh]).\n[\u003c\u003c\"sh: can't fork\\n\"\u003e\u003e]\n\n% If we check the parent for events, we can see the child has exited\n10\u003e alcove:event(P, []).\n{signal,sigchld}\n```\n\n## Creating a Container Using Linux Namespaces\n\nNamespaces are the basis for Linux Containers (LXC). Creating a\nnew namespace is as simple as passing in the appropriate flags to\nclone(2). We'll rewrite the chroot example to run inside a namespace and\nuse another Linux feature, control groups, to limit the system resources\navailable to the process.\n\nSee `examples/nsex.erl`.\n\n* set process limits using cgroups (see cpuset(7))\n\n  When the port is started, we'll create a new cgroup just for our\n  application and, whenever a sandboxed process is forked, we'll add it\n  to this cgroup.\n\n```erlang\nstart() -\u003e\n    {ok, Drv} = alcove_drv:start([{exec, \"sudo -n\"}]),\n\n    % Create a new cgroup for our processes\n    ok = alcove_cgroup:create(Drv, [], \u003c\u003c\"alcove\"\u003e\u003e),\n\n    % Set the CPUs these processes are allowed to run on. For example,\n    % if there are 4 available CPUs, any process in this cgroup will only\n    % be able to run on CPU 0\n    {ok, 1} = alcove_cgroup:set(\n        Drv,\n        [],\n        \u003c\u003c\"cpuset\"\u003e\u003e,\n        \u003c\u003c\"alcove\"\u003e\u003e,\n        \u003c\u003c\"cpuset.cpus\"\u003e\u003e,\n        \u003c\u003c\"0\"\u003e\u003e\n    ),\n    {ok, 1} = alcove_cgroup:set(\n        Drv,\n        [],\n        \u003c\u003c\"cpuset\"\u003e\u003e,\n        \u003c\u003c\"alcove\"\u003e\u003e,\n        \u003c\u003c\"cpuset.mems\"\u003e\u003e,\n        \u003c\u003c\"0\"\u003e\u003e\n    ),\n\n    % Set the amount of memory available to the process\n\n    % Total memory, including swap. We allow this to fail, because some\n    % systems may not have a swap partition/file\n    alcove_cgroup:set(\n        Drv,\n        [],\n        \u003c\u003c\"memory\"\u003e\u003e,\n        \u003c\u003c\"alcove\"\u003e\u003e,\n        \u003c\u003c\"memory.memsw.limit_in_bytes\"\u003e\u003e,\n        \u003c\u003c\"16m\"\u003e\u003e\n    ),\n\n    % Total memory\n    {ok, 3} = alcove_cgroup:set(\n        Drv,\n        [],\n        \u003c\u003c\"memory\"\u003e\u003e,\n        \u003c\u003c\"alcove\"\u003e\u003e,\n        \u003c\u003c\"memory.limit_in_bytes\"\u003e\u003e,\n        \u003c\u003c\"16m\"\u003e\u003e\n    ),\n\n    Drv.\n\nsetlimits(Drv, Child) -\u003e\n    % Add our process to the \"alcove\" cgroup\n    {ok, _} = alcove_cgroup:set(\n        Drv,\n        [],\n        \u003c\u003c\u003e\u003e,\n        \u003c\u003c\"alcove\"\u003e\u003e,\n        \u003c\u003c\"tasks\"\u003e\u003e,\n        integer_to_list(Child)\n    ).\n```\n\n* running the code involves calling clone(2) to create the namespaces,\n  rather than using fork(2)\n\n```erlang\nsandbox(Drv, Argv) -\u003e\n    {Path, Arg0, Args} = argv(Argv),\n\n    {ok, Child} = alcove:clone(Drv, [], [\n        % IPC\n        clone_newipc,\n        % network\n        clone_newnet,\n        % mounts\n        clone_newns,\n        % PID, Child is PID 1 in the namespace\n        clone_newpid,\n        % hostname\n        clone_newuts\n    ]),\n\n    setlimits(Drv, Child),\n    chroot(Drv, Child, Path),\n    drop_privs(Drv, Child, id()),\n\n    ok = alcove:execvp(Drv, [Child], Arg0, [Arg0, Args]),\n\n    Child.\n```\n\n## Operating System Support\n\nFunctions marked as operating system specific raise an undefined function\nerror on unsupported platforms.\n\n## Event Loop\n\nThese functions can be called while the process is running in the event\nloop. Using these functions after the process has called exec(3) will\nprobably confuse the process.\n\nFunctions accepting a constant() will return {error, enotsup} if an\natom is used as the argument and is not found on the platform.\n\nThe alcove module functions accept an additional argument which allows\nsetting timeouts. For example:\n\n```\nwrite(Drv, Pipeline, FD, Buf) -\u003e {ok, Count} | {error, posix()}\nwrite(Drv, Pipeline, FD, Buf, timeout()) -\u003e {ok, Count} | {error, posix()}\n```\n\nBy default, timeout is set to infinity. Similar to gen_server:call/3,\nsetting an integer timeout will cause the process to crash if the\ntimeout is reached. If the failure is caught, the caller must deal\nwith any delayed messages that arrive for the Unix process described by\nthe pipeline.\n\nSee \"Message Format\" for a description of the messages.\n\n## Message Format\n\n* synchronous replies to calls from alcove processes running in the\n  event loop. The type of the last element of the tuple depends on the call\n  (e.g., open/4,5 would return either {ok, integer()} or {error, posix()}\n\n  ```\n    {alcove_call, pid(), [non_neg_integer()], term()}\n  ```\n\n* asynchronous events generated by the alcove process (e.g., signals).\n\n  ```\n    {alcove_event, pid(), [non_neg_integer()], term()}\n  ```\n\n* standard error: can be generated by an alcove process running in the\n  event loop as well as a Unix process.\n\n  ```\n    {alcove_stderr, pid(), [non_neg_integer()], binary()}\n  ```\n\n* standard output: output from the Unix process after alcove has called\n  exec(3)\n\n  ```\n    {alcove_stdout, pid(), [non_neg_integer()], binary()}\n  ```\n\n## Examples\n\nTo compile the examples:\n\n```\nmake eg\n```\n\n* GPIO\n\n`examples/gpioled.erl` is a simple example of interacting with the GPIO\non a beaglebone black or raspberry pi that will blink an LED. The example\nworks with two system processes:\n\n```\n* a port process which requests the GPIO pin be exported to user\n  space, forks a child into a new namespace, then drops privileges\n\n* a child process gets a file descriptor for the GPIO, then drops\n  privileges\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsantos%2Falcove","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmsantos%2Falcove","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsantos%2Falcove/lists"}