{"id":18151225,"url":"https://github.com/msantos/stdio","last_synced_at":"2025-04-28T17:41:49.044Z","repository":{"id":52238777,"uuid":"516726696","full_name":"msantos/stdio","owner":"msantos","description":"Reliably reap, restrict and isolate system tasks: Stdio is a control plane for processes","archived":false,"fork":false,"pushed_at":"2024-08-10T12:42:22.000Z","size":238,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-30T11:41:35.449Z","etag":null,"topics":["capsicum","exec","fork","inetd","linux-namespaces","pledge","prctl","procctl","seccomp","signal","stdio","supervisor"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","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,"publiccode":null,"codemeta":null}},"created_at":"2022-07-22T11:34:37.000Z","updated_at":"2025-01-12T02:55:01.000Z","dependencies_parsed_at":"2024-08-04T15:24:35.443Z","dependency_job_id":"3c6406fa-cbf5-4df6-bee9-c1e5f3b9e1c6","html_url":"https://github.com/msantos/stdio","commit_stats":{"total_commits":20,"total_committers":1,"mean_commits":20.0,"dds":0.0,"last_synced_commit":"0741678f0eecbb5fb10f93691095a8f82029b06c"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Fstdio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Fstdio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Fstdio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msantos%2Fstdio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/msantos","download_url":"https://codeload.github.com/msantos/stdio/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251357563,"owners_count":21576731,"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","inetd","linux-namespaces","pledge","prctl","procctl","seccomp","signal","stdio","supervisor"],"created_at":"2024-11-02T01:07:01.492Z","updated_at":"2025-04-28T17:41:49.024Z","avatar_url":"https://github.com/msantos.png","language":"Elixir","readme":"# Stdio\n\n[![Package Version](https://img.shields.io/hexpm/v/stdio)](https://hex.pm/packages/stdio)\n[![Hex Docs](https://img.shields.io/badge/hex-docs)](https://hexdocs.pm/stdio/)\n\nStream standard I/O from system processes.\n\nReliably reap, restrict and isolate system tasks:\n[Stdio](https://github.com/msantos/stdio) is a control plane for processes.\n\n```mermaid\ngraph LR\n    B([beam]) --\u003e|root| S0[Supervise]\n    B([beam]) --\u003e|user| S1[Supervise]\n    S0 --\u003e |user|I0[[init]]\n    S0 --\u003e |user|I1[[init]]\n    subgraph namespace0\n    I0 --\u003e C0[[echo 'Hello']]\n    end\n    subgraph namespace1\n    I1 --\u003e C1[[echo 'System working?']]\n    end\n    S1 --\u003e C2[[echo 'Seems to be.']]\n```\n\n## Installation\n\n`Stdio` is an Elixir library. The package can be installed by adding\n`stdio` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:stdio, \"~\u003e 0.4.4\"}\n  ]\nend\n```\n\n## Usage\n\n```elixir\niex\u003e Stdio.stream!(\"echo test\") |\u003e Enum.to_list()\n[stdout: \"test\\n\", exit_status: 0]\n```\n\nCommands use `/bin/sh -c`:\n\n```elixir\niex\u003e Stdio.stream!(\"pstree $$\") |\u003e Enum.to_list()\n[stdout: \"sh---pstree\\n\", exit_status: 0]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003eS[Supervise]\n    S --\u003e I[[sh]]\n    I --\u003e C[[pstree]]\n```\n\n### Pipes\n\nStreams can be piped into processes:\n\n```elixir\niex\u003e [\"let\", \"it\", \"crash\"] |\u003e Stdio.pipe!(\"tr '[a-z]' '[A-Z]'\") |\u003e Enum.to_list()\n[stdout: \"LETITCRASH\", exit_status: 0]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003e|\"[let, it, crash]\"|S[Supervise]\n    S --\u003e I[[sh]]\n    I --\u003e |\"[let, it, crash]\"|C[[\"tr '[a-z]' '[A-Z]'\"]]\n    C --\u003e O([\"LETITCRASH\"])\n```\n\n### Argument List\n\nUse an argv to execute a command without an intermediary shell\nprocess. `$PATH` is not consulted: the path to the executable is required.\n\n```elixir\niex\u003e Stdio.stream!([\"/bin/echo\", \"test\"]) |\u003e Enum.to_list()\n[stdout: \"test\\n\", exit_status: 0]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003eS[Supervise]\n    S --\u003e C[[/bin/echo test]]\n```\n\n### Background Processes\n\nBackground and daemonized processes are reaped when the foreground\nprocess exits:\n\n```elixir\niex\u003e Stdio.stream!(\"sleep 131 \u0026 sleep 111 \u0026 echo $$\") |\u003e Enum.to_list()\n[stdout: \"25723\\n\", exit_status: 0]\niex\u003e Stdio.Procfs.children(25723)\n[]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003eS[Supervise]\n    S --- I[[/bin/sh]]\n    I -.- C0[[sleep 131]]\n    I -.- C1[[sleep 111]]\n    I --- C2[[echo $$]]\n    S --\u003e C0\n    S --\u003e C1\n```\n\n### Setuid Binaries\n\nSetuid processes are disabled by default to prevent unkillable processes.\n\n```elixir\niex\u003e Stdio.stream!(\"ping -c 1 8.8.8.8\") |\u003e Enum.to_list()\n[stderr: \"ping: ssend socket: Operation not permitted\\n\", exit_status: 71]\n```\n\nThe process can escalate privileges by using the `setuid` option:\n\n```elixir\niex\u003e Stdio.stream!(\"ping -c 1 8.8.8.8\", Stdio.Process, setuid: true) |\u003e Enum.to_list()\n[\n  stdout: \"PING 8.8.8.8 (8.8.8.8): 56 data bytes\\n64 bytes from 8.8.8.8: icmp_seq=0 ttl=116 time=1.726 ms\\n\",\n  stdout: \"\\n--- 8.8.8.8 ping statistics ---\\n1 packets transmitted, 1 packets received, 0.0% packet loss\\nround-trip min/avg/max/stddev = 1.726/1.726/1.726/0.000 ms\\n\",\n  exit_status: 0\n]\n```\n\n### Privileges\n\n\u003e [!IMPORTANT]\n\u003e\n\u003e Some behaviours may require running system processes as the root user.\n\u003e\n\u003e For setup, see\n\u003e [Stdio.setuid/0](https://hexdocs.pm/stdio/Stdio.html#setuid/0).\n\nBehaviours may change the root filesystem for the process. The default\n`chroot(2)` directory hierarchy can be created by running:\n\n```elixir\niex\u003e Stdio.Container.make_chroot_tree!()\n```\n\n### Process Isolation\n\nBehaviours can implement process restrictions or process isolation. For\nexample, by default the `Stdio.Rootless` behaviour does not have network\naccess:\n\n```elixir\niex\u003e Stdio.stream!(\"ip -br addr show\", Stdio.Rootless) |\u003e Enum.to_list()\n[stdout: \"lo               DOWN           \\n\", exit_status: 0]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003e|user| S[Supervise]\n    S --\u003e |user|I[[init]]\n    subgraph \"user/network namespace\"\n    I --\u003e C[[ip -br addr show]]\n    end\n```\n\n### Linux Container\n\nThe `Stdio.Container` behaviour also disables network access:\n\n```elixir\niex\u003e Stdio.stream!(\"ping -c 1 8.8.8.8\", Stdio.Container, setuid: true) |\u003e Enum.to_list()\n[stderr: \"ping: connect: Network is unreachable\\n\", exit_status: 2]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003e|root| S[Supervise]\n    S --\u003e |user|I[[sh]]\n    subgraph \"network namespace\"\n    I --\u003e C[[ping -c 1 8.8.8.8]]\n    end\n```\n\nIf `setuid` is allowed and the `host` network is shared, `ping` works\nas expected:\n\n```elixir\niex\u003e Stdio.stream!(\"ping -c 1 8.8.8.8\", Stdio.Container, setuid: true, net: :host) |\u003e Enum.to_list()\n[\n  stdout: \"PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=32.4 ms\\n\\n--- 8.8.8.8 ping statistics ---\\n1 packets transmitted, 1 received, 0% packet loss, time 0ms\\nrtt min/avg/max/mdev = 32.390/32.390/32.390/0.000 ms\\n\",\n  exit_status: 0\n]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003e|root| S[Supervise]\n    S --\u003e |user|I[[sh]]\n    subgraph \"namespace (net shared with parent)\"\n    I --\u003e C[[ping -c 1 8.8.8.8]]\n    end\n```\n\n### FreeBSD Jails\n\nFreeBSD Jails work in a similar way. An argv is used because the process\nroot directory has been changed to `/rescue`, a directory containing\nstatically linked binaries. By default `setuid` and `net` are disabled:\n\n```elixir\niex\u003e Stdio.stream!([\"./ping\", \"-c\", \"1\", \"8.8.8.8\"], Stdio.Jail, path: \"/rescue\") |\u003e Enum.to_list()\n[stderr: \"ping: ssend socket: Protocol not supported\\n\", exit_status: 71]\n```\n\n```mermaid\ngraph LR\n    B([beam]) --\u003e|root| S[Supervise]\n    S --\u003e |user|I[[sh]]\n    subgraph \"jail\"\n    I --\u003e C[[ping -c 1 8.8.8.8]]\n    end\n```\n\n## Examples\n\n### inetd\n\nThe following code is an example of an `inetd(8)` service using forked system\nprocesses to handle client requests. It is adapted from the [Task and\ngen_tcp](https://elixir-lang.org/getting-started/mix-otp/task-and-gen-tcp.html)\necho server in the Elixir documentation.\n\nTo run from iex:\n\n```elixir\niex\u003e Inetd.Server.start([\n...\u003e %{port: 7070, command: \"cat -n\"},\n...\u003e %{ip: \"127.0.0.1\", port: 7071, command: \"stdbuf -o0 tr [A-Z] [a-z]\"}\n...\u003e ])\n```\n\n```elixir\ndefmodule Inetd.Server do\n  require Logger\n\n  def start(spec) do\n    children =\n      spec\n      |\u003e Enum.map(fn %{port: port, command: command} = m -\u003e\n        {:ok, ip} = :inet_parse.address(String.to_charlist(Map.get(m, :ip, \"::\")))\n        behaviour = Map.get(m, :behaviour, Stdio.Rootless)\n\n        Supervisor.child_spec(\n          {Task, fn -\u003e Inetd.Server.accept(ip, port, command, behaviour) end},\n          id: {ip, port},\n          restart: :permanent\n        )\n      end)\n\n    children = [{Task.Supervisor, name: Inetd.TaskSupervisor}] ++ children\n\n    opts = [strategy: :one_for_one, name: Inetd.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n\n  @doc \"\"\"\n  Starts accepting connections on the given `port`.\n  \"\"\"\n  def accept(ip, port, command, behaviour) do\n    {:ok, socket} =\n      :gen_tcp.listen(\n        port,\n        [:binary, packet: :line, active: false, reuseaddr: true, ip: ip]\n      )\n\n    Logger.info(\"Accepting connections on port #{port}\")\n    loop_acceptor(socket, command, behaviour)\n  end\n\n  defp loop_acceptor(socket, command, behaviour) do\n    {:ok, client} = :gen_tcp.accept(socket)\n\n    {:ok, pid} =\n      Task.Supervisor.start_child(Inetd.TaskSupervisor, fn -\u003e\n        serve(client, command, behaviour)\n      end)\n\n    :ok = :gen_tcp.controlling_process(client, pid)\n    loop_acceptor(socket, command, behaviour)\n  end\n\n  defp serve(socket, command, behaviour) do\n    Stream.resource(\n      fn -\u003e socket end,\n      fn socket -\u003e read_line(socket) end,\n      fn socket -\u003e :gen_tcp.close(socket) end\n    )\n    |\u003e Stdio.pipe!(command, behaviour)\n    |\u003e Stream.transform(socket, fn data, socket -\u003e\n      write_line(data, socket)\n    end)\n    |\u003e Stream.run()\n  end\n\n  defp read_line(socket) do\n    case :gen_tcp.recv(socket, 0) do\n      {:ok, data} -\u003e\n        {[data], socket}\n\n      {:error, _} -\u003e\n        {:halt, socket}\n    end\n  end\n\n  defp write_line({:stdout, line}, socket) do\n    case :gen_tcp.send(socket, line) do\n      :ok -\u003e\n        {[], socket}\n\n      {:error, _} -\u003e\n        {:halt, socket}\n    end\n  end\n\n  defp write_line({:stderr, line}, socket) do\n    case :gen_tcp.send(socket, line) do\n      :ok -\u003e\n        {[], socket}\n\n      {:error, _} -\u003e\n        {:halt, socket}\n    end\n  end\n\n  defp write_line({msg, _} = data, socket) when msg in [:exit_status, :termsig] do\n    Logger.info(\"#{inspect(data)}\")\n    {:halt, socket}\n  end\nend\n```\n\n## Documentation\n\nDocumentation is available on [hexdocs](https://hexdocs.pm/stdio/).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsantos%2Fstdio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmsantos%2Fstdio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsantos%2Fstdio/lists"}