From be438c62e1bd62a1fa6882e93f03538d590d8204 Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Fri, 22 Aug 2025 13:46:59 +0200 Subject: [PATCH] nix/eval: remove --write-to it's broken, can write arbitrary file paths when run as root, and only supports strings and recursive sets of strings. this was only used for manpage generation in a build system that has not woken up since 1976. fixes #974 fixes #227 Change-Id: I4f18599685a3077c15ddc02c759558f986c8c6e4 --- doc/manual/generate-manpage.nix | 2 +- doc/manual/rl-next/remove-write-to.md | 14 +++++++ lix/libutil/args.cc | 1 + lix/libutil/args.hh | 2 + lix/nix/eval.cc | 59 ++++++++------------------- lix/nix/eval.md | 14 ------- tests/functional/pure-eval.sh | 8 ---- 7 files changed, 35 insertions(+), 65 deletions(-) create mode 100644 doc/manual/rl-next/remove-write-to.md diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 4abf7c302..5e6e99f26 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -225,7 +225,7 @@ let showCategory = cat: '' ${optionalString (cat != "") "**${cat}:**"} - ${listOptions (filterAttrs (n: v: v.category == cat) allOptions)} + ${listOptions (filterAttrs (n: v: v.category == cat && !v.hidden) allOptions)} ''; listOptions = opts: concatStringsSep "\n" (attrValues (mapAttrs showOption opts)); showOption = diff --git a/doc/manual/rl-next/remove-write-to.md b/doc/manual/rl-next/remove-write-to.md new file mode 100644 index 000000000..dad5e541e --- /dev/null +++ b/doc/manual/rl-next/remove-write-to.md @@ -0,0 +1,14 @@ +--- +synopsis: "`nix eval --write-to` has been removed" +cls: [4045] +issues: [fj#974, fj#227] +category: "Breaking Changes" +credits: [horrors] +--- + +`nix eval --write-to` has been removed since it was underspecified, not widely +useful, and prone to security-sensitive misbehaviors. The feature was added in +Nix 2.4 purely for internal use in the build system. According to our research +it hasn't found any use outside of some distribution packaging scripts. Please +use structured outputs formats (such as JSON) instead as they have better type +fidelity, don't conflate attributes with paths, and are useful to other tools. diff --git a/lix/libutil/args.cc b/lix/libutil/args.cc index 9a75b99ee..6c58e6821 100644 --- a/lix/libutil/args.cc +++ b/lix/libutil/args.cc @@ -299,6 +299,7 @@ JSON Args::toJSON() if (!flag->labels.empty()) j["labels"] = flag->labels; j["experimental-feature"] = flag->experimentalFeature; + j["hidden"] = flag->hidden; flags[name] = std::move(j); } diff --git a/lix/libutil/args.hh b/lix/libutil/args.hh index 86814e70f..78eede412 100644 --- a/lix/libutil/args.hh +++ b/lix/libutil/args.hh @@ -168,6 +168,8 @@ protected: Strings labels; Handler handler; CompleterClosure completer; + /// Whether to hide this flag in generated documentation and CLI specifications. + bool hidden = false; std::optional experimentalFeature; diff --git a/lix/nix/eval.cc b/lix/nix/eval.cc index beebd1a70..2adc4bf51 100644 --- a/lix/nix/eval.cc +++ b/lix/nix/eval.cc @@ -32,11 +32,15 @@ struct CmdEval : MixJSON, InstallableCommand, MixReadOnlyOption .handler = {&apply}, }); + // `--write-to` was axed because it was not used in-tree, no non-packaging uses out of tree + // could be found, and it was rife with misvehavior including arbitrary file writes as root + // when run a prepared input. we have opted to remove it instead of trying to make it safe. addFlag({ .longName = "write-to", - .description = "Write a string or attrset of strings to *path*.", + .description = "Previously used to write a string or attrset of strings to *path*.", .labels = {"path"}, .handler = {&writeTo}, + .hidden = true, }); } @@ -59,6 +63,13 @@ struct CmdEval : MixJSON, InstallableCommand, MixReadOnlyOption if (raw && json) throw UsageError("--raw and --json are mutually exclusive"); + if (writeTo) { + throw UsageError( + "--write-to has been removed because it was insecure and broken, please use " + "structured output formats (e.g. via --json) instead" + ); + } + auto const installableValue = InstallableValue::require(installable); auto evaluator = getEvaluator(); @@ -75,54 +86,18 @@ struct CmdEval : MixJSON, InstallableCommand, MixReadOnlyOption v = vRes; } - if (writeTo) { - logger->pause(); - - if (pathExists(*writeTo)) - throw Error("path '%s' already exists", *writeTo); - - std::function recurse; - - recurse = [&](Value & v, const PosIdx pos, const Path & path, NeverAsync) - { - state->forceValue(v, pos); - if (v.type() == nString) - // FIXME: disallow strings with contexts? - writeFile(path, v.str()); - else if (v.type() == nAttrs) { - if (mkdir(path.c_str(), 0777) == -1) - throw SysError("creating directory '%s'", path); - for (auto & attr : *v.attrs) { - std::string_view name = evaluator->symbols[attr.name]; - try { - if (name == "." || name == "..") - throw Error("invalid file name '%s'", name); - recurse(*attr.value, attr.pos, concatStrings(path, "/", name), {}); - } catch (Error & e) { - e.addTrace( - evaluator->positions[attr.pos], - HintFmt("while evaluating the attribute '%s'", name)); - throw; - } - } - } - else - evaluator->errors.make("value at '%s' is not a string or an attribute set", evaluator->positions[pos]).debugThrow(); - }; - - recurse(*v, pos, *writeTo, {}); - } - - else if (raw) { + if (raw) { logger->pause(); writeFull(STDOUT_FILENO, *state->coerceToString(noPos, *v, context, "while generating the eval command output")); } - else if (json) { + else if (json) + { logger->cout("%s", printValueAsJSON(*state, true, *v, pos, context, false)); } - else { + else + { logger->cout( "%s", ValuePrinter( diff --git a/lix/nix/eval.md b/lix/nix/eval.md index 0e2b70b94..599d7da1f 100644 --- a/lix/nix/eval.md +++ b/lix/nix/eval.md @@ -41,15 +41,6 @@ R""( # nix eval nix#checks.x86_64-linux --apply builtins.attrNames ``` -* Generate a directory with the specified contents: - - ```console - # nix eval --write-to ./out --expr '{ foo = "bar"; subdir.bla = "123"; }' - # cat ./out/foo - bar - # cat ./out/subdir/bla - 123 - # Description This command evaluates the given Nix expression and prints the @@ -76,9 +67,4 @@ result on standard output. The output is printed exactly as-is, with no quotes, escaping, or trailing newline. -* With `--write-to` *path*, the evaluation result must be a string or - a nested attribute set whose leaf values are strings. These strings - are written to files named *path*/*attrpath*. *path* must not - already exist. - )"" diff --git a/tests/functional/pure-eval.sh b/tests/functional/pure-eval.sh index 7940c43b7..0eefae288 100644 --- a/tests/functional/pure-eval.sh +++ b/tests/functional/pure-eval.sh @@ -24,12 +24,4 @@ echo "$missingImpureErrorMsg" | grepQuiet -- --impure || \ (! nix eval --expr "(import (builtins.fetchurl { url = \"file://$(pwd)/pure-eval.nix\"; })).x") nix eval --expr "(import (builtins.fetchurl { url = \"file://$(pwd)/pure-eval.nix\"; sha256 = \"$(nix hash file pure-eval.nix --type sha256)\"; })).x" -rm -rf $TEST_ROOT/eval-out -nix eval --store dummy:// --write-to $TEST_ROOT/eval-out --expr '{ x = "foo" + "bar"; y = { z = "bla"; }; }' -[[ $(cat $TEST_ROOT/eval-out/x) = foobar ]] -[[ $(cat $TEST_ROOT/eval-out/y/z) = bla ]] - -rm -rf $TEST_ROOT/eval-out -(! nix eval --store dummy:// --write-to $TEST_ROOT/eval-out --expr '{ "." = "bla"; }') - (! nix eval --expr '~/foo')