erlexec

repository·master·Indexed 20 days ago

https://github.com/saleyn/erlexec

An OS process manager for the Erlang VM that provides fine-grained control over OS processes, offering more features than the built-in erlang:open_port/2 with {spawn, Command}. It supports Erlang and Elixir, allowing for the management of child OS processes, environment variable configuration, PTY usage, and Linux capabilities. The architecture utilizes a C++ port program (exec-port) to bridge Erlang light-weight Pids with managed OS processes.

Tokens
2.4K
Snippets
13
Records
15
Agent score
20%

What's inside erlexec

  1. How erlexec architecture works

    master

    Erlexec manages OS processes through a multi-layered architecture:

    1. Erlang light-weight Pids: These are one-to-one associated with managed OS PIDs and are linked to the exec application.
    2. Exec application: Runs within the Erlang VM.
    3. exec-port: A separate OS port program that acts as the bridge.
    4. Managed Child OS processes: The actual OS processes being controlled (can include STDIN/STDOUT/STDERR pipes).
  2. Allow erlexec to execute commands as multiple specific users

    master

    If the exec-port program is started with root privileges (via SUID or sudo), it can switch to different effective users for specific commands.

    To restrict which users can be used, provide a list of allowed users via the {limit_users, Users} option in exec:start/1. When running a command, specify the target user with {user, "Username"} in the exec:run/2 options.

    %% Start with root and limit allowed users
    1> Opts = [root, {user, "wheel"}, {limit_users, ["alex","guest"]}],
    2> exec:start(Opts).
    
    %% Run a command as "alex"
    3> exec:run("whoami", [sync, stdout, {user, "alex"}]).
  3. Run commands using a Pseudo-Terminal (PTY)

    master

    You can run commands within a PTY by using the pty option.

    Important: PTY stdout/stderr Separation In PTY mode, stdout and stderr are multiplexed into a single stream. If you specify both stdout and stderr options, all output will go to stdout, and stderr will receive nothing.

    Workaround: To see both streams in a single coherent order, explicitly redirect stderr to stdout using {stderr, stdout}.

    %% Recommended way to see all output in PTY mode
    exec:run("command", [stdin, stdout, {stderr, stdout}, pty]).
  4. Import erlexec in Erlang

    master

    To use erlexec as a dependency in an Erlang project, add it to your rebar.config and include it in your application's .app.src file.

    % In rebar.config
    {deps,
     [% ...
      {erlexec, "~> 2.0"}
      ]}.
    
    % In your_app.app.src
    {applications,
       [kernel,
        stdlib,
        % ...
        erlexec
       ]}
  5. Start and stop OS processes in Erlang and Elixir

    master

    You can manage OS processes using the exec module in Erlang or the :exec module in Elixir.

    In Erlang, use exec:start() to initialize the port program, exec:run_link/2 to run a command and link it to the current process, and exec:stop/1 to terminate a process by its PID.

    In Elixir, use :exec.start to initialize and :exec.run/2 to execute commands.

    %% Erlang
    1> exec:start().
    2> {ok, _, I} = exec:run_link("sleep 1000", []).
    3> exec:stop(I).
    
    %% Elixir
    iex(1)> :exec.start
    iex(2)> :exec.run("echo ok", [:sync, :stdout])
  6. Run erlexec as a different effective user

    master

    To run the exec-port program as a different user, the current user must have sudo rights or the exec-port binary must be owned by root with the SUID bit set (chmod 4555).

    If the exec-port binary is not accessible in the real user's directory, specify its location using the {portexe, "/path/to/exec-port"} option during exec:start/1.

    %% Setup SUID (Linux)
    $ chown root:root exec-port; chmod 4555 exec-port
    
    %% Start as effective user "wheel"
    1> exec:start([{user, "wheel"}, {portexe, "/tmp/exec-port"}]).
  7. Build erlexec from source

    master

    Ensure rebar or rebar3 is installed and in your PATH.

    Linux User Switching Requirements: If you intend to use Linux capabilities to run tasks as different effective user IDs, you must either install libcap-dev (or libcap-devel on Fedora/CentOS) or ensure the user running the port program has sudo rights.

    Build Commands:

    $ git clone git@github.com:saleyn/erlexec.git
    $ make

    Custom Build Options:

    • To disable optimized build of exec-port: OPTIMIZE=0 make
    • To use select(2) instead of the default poll(2) for event demultiplexing: USE_POLL=0 make
  8. Configure the SHELL environment variable

    master
    The exec-port program requires the SHELL environment variable to be set. If you are running Erlang inside a Docker container, ensure that SHELL is properly set before starting the emulator to avoid execution errors.
  9. Communicate with an OS process via STDIN

    master

    To send data to a running process's standard input, use exec:send/2.

    To signal the end-of-file (EOF) to the child process, send the eof atom: exec:send(Pid, eof). This is necessary for processes that wait for EOF to finish processing (like tac).

    %% Send data
    exec:send(Pid, <<"data\n">>).
    
    %% Send EOF
    exec:send(Pid, eof).
  10. Kill a process group at process exit

    master

    You can ensure that a child process and all its descendants are terminated by using the group and kill_group options.

    1. Start a process with {group, GID} (where GID is the OS PID of the process).
    2. Start subsequent processes with the same {group, GID}.
    3. Use the kill_group option on the primary process. When the primary process exits, all processes in that group will receive a SIGTERM (signal 15).
    %% Start a process group
    {ok, P2, GID} = exec:run("sleep 10", [{group, 0}, kill_group]).
    
    %% Join the group
    {ok, P3, _} = exec:run("sleep 15", [{group, GID}, monitor]).
  11. Use Linux Capabilities with erlexec

    master

    On Linux, you can grant the exec-port program specific kernel capabilities using the {capabilities, Capabilities} option in exec:start/1. These capabilities are automatically propagated to child processes.

    Available Capabilities (prefix cap_ is optional):

    • setuid (cap_setuid): Change UID/GID.
    • kill (cap_kill): Send signals.
    • sys_nice (cap_sys_nice): Set process priority.
    • net_bind_service (cap_net_bind_service): Bind to ports < 1024.
    • net_admin (cap_net_admin): Network administration.
    • all: Enables all available capabilities.

    If no capabilities option is provided, the default set is [setuid, kill, sys_nice].

    %% Start with specific capabilities
    1> Opts = [{capabilities, [setuid, kill, sys_nice]}],
    2> exec:start(Opts).
    
    %% Enable all capabilities
    1> exec:start([{capabilities, all}]).