Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent bad vm.args in Nerves firmware #884

Merged
merged 2 commits into from Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
83 changes: 83 additions & 0 deletions lib/nerves/release.ex
Expand Up @@ -16,6 +16,8 @@ defmodule Nerves.Release do
steps: release.steps ++ [&Nerves.Release.finalize/1]
}

check_vm_args_compatibility!(release)

_ = File.rm_rf!(release.path)

if Code.ensure_loaded?(Shoehorn.Release) do
Expand Down Expand Up @@ -167,4 +169,85 @@ defmodule Nerves.Release do
{to_string(app), to_string(opts[:vsn]), Path.expand(opts[:path] || "")}
end
end

@elixir_1_15_opts ["-user elixir", "-run elixir start_iex"]
@legacy_elixir_opts ["-user Elixir.IEx.CLI"]
defp check_vm_args_compatibility!(release) do
Mix.shell().info([:yellow, "* [Nerves] ", :reset, "validating vm.args"])
vm_args_path = Mix.Release.rel_templates_path(release, "vm.args.eex")

if not File.exists?(vm_args_path) do
Mix.raise("Missing required #{vm_args_path}")
end

{exclusions, inclusions} =
if Version.match?(System.version(), ">= 1.15.0") do
{@legacy_elixir_opts, @elixir_1_15_opts}
else
{@elixir_1_15_opts, @legacy_elixir_opts}
end

vm_args = File.read!(vm_args_path)

errors =
[]
|> check_vm_args_inclusions(vm_args, inclusions, vm_args_path)
|> check_vm_args_exclusions(vm_args, exclusions, vm_args_path)

if length(errors) > 0 do
errs = IO.ANSI.format(errors) |> IO.chardata_to_string()

Mix.raise("""
Incompatible vm.args.eex

The procedure for starting IEx changed in Elixir 1.15. The rel/vm.args.eex for
this project starts IEx in an incompatible way for the version of Elixir you're
using and won't work.

To fix this, either change the version of Elixir that you're using or make the
following changes to vm.args.eex:
#{errs}
""")
else
:ok
end
end

defp check_vm_args_exclusions(errors, vm_args, exclusions, vm_args_path) do
String.split(vm_args, "\n")
|> Enum.with_index(1)
|> Enum.filter(fn {line, _} -> Enum.any?(exclusions, &String.contains?(line, &1)) end)
|> case do
[] ->
[]

lines ->
[
"\nPlease remove the following lines:\n\n",
Enum.map(lines, fn {line, line_num} ->
["* ", vm_args_path, ":", to_string(line_num), ":\n ", :red, line, "\n"]
end)
| errors
]
end
end

defp check_vm_args_inclusions(errors, vm_args, inclusions, vm_args_path) do
case Enum.reject(inclusions, &String.contains?(vm_args, &1)) do
[] ->
[]

lines ->
[
[
"\nPlease ensure the following lines are in ",
vm_args_path,
":\n",
:green,
Enum.map(lines, &[" ", &1, "\n"])
]
| errors
]
end
end
end
1 change: 1 addition & 0 deletions test/fixtures/release_app/mix.exs
Expand Up @@ -27,6 +27,7 @@ defmodule ReleaseApp.Fixture do
[
overwrite: true,
steps: [&Nerves.Release.init/1, :assemble],
rel_templates_path: System.get_env("REL_TEMPLATES_PATH", "rel"),
strip_beams: true
]
end
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/release_app/rel/vm.args.eex
@@ -0,0 +1 @@
# File required by Nerves
68 changes: 68 additions & 0 deletions test/nerves/release_test.exs
Expand Up @@ -26,4 +26,72 @@ defmodule Nerves.ReleaseTest do
|> File.read!()
|> String.starts_with?(expected)
end

@tag :tmp_dir
@tag :release
test "requires vm.args.eex", %{tmp_dir: tmp} do
{path, env} = compile_fixture!("release_app", tmp, [], [])

opts = [
cd: path,
env: [{"MIX_ENV", "prod"}, {"REL_TEMPLATES_PATH", Path.join(tmp, "no-rel")} | env],
stderr_to_stdout: true
]

assert {output, 1} = System.cmd("mix", ["release"], opts)

assert output =~ ~r/Missing required .*vm\.args\.eex/
end

@tag :tmp_dir
@tag :release
test "fails if vm.args has incompatible shell setting", %{tmp_dir: tmp} do
{path, env} = compile_fixture!("release_app", tmp, [], [])

rel_templates_path = Path.join(tmp, "bad_rel")
assert :ok = File.mkdir_p(rel_templates_path)
bad_vm_args = Path.join(rel_templates_path, "vm.args.eex")

expected =
if Version.match?(System.version(), ">= 1.15.0") do
assert :ok = File.write(bad_vm_args, "# test.vm.args\n-user Elixir.IEx.CLI")

~r"""
Please remove the following lines:

\* #{bad_vm_args}:2:
-user Elixir.IEx.CLI

Please ensure the following lines are in #{bad_vm_args}:
-user elixir
-run elixir start_iex
"""
else
assert :ok =
File.write(bad_vm_args, "# test.vm.args\n-user elixir\n-run elixir start_iex")

~r"""
Please remove the following lines:

\* #{bad_vm_args}:2:
-user elixir
\* #{bad_vm_args}:3:
-run elixir start_iex

Please ensure the following lines are in #{bad_vm_args}:
-user Elixir.IEx.CLI
"""
end

opts = [
cd: path,
env: [{"MIX_ENV", "prod"}, {"REL_TEMPLATES_PATH", rel_templates_path} | env],
stderr_to_stdout: true
]

{output, 1} = System.cmd("mix", ["release"], opts)

assert output =~ "Incompatible vm.args"
assert output =~ expected
end
end