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
This commit is contained in:
eldritch horrors
2025-08-23 10:39:30 +00:00
parent 0f50bc452e
commit be438c62e1
7 changed files with 35 additions and 65 deletions
+1 -1
View File
@@ -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 =
+14
View File
@@ -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.
+1
View File
@@ -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);
}
+2
View File
@@ -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> experimentalFeature;
+17 -42
View File
@@ -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<void(Value & v, const PosIdx pos, const Path & path, NeverAsync)> 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<TypeError>("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(
-14
View File
@@ -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.
)""
-8
View File
@@ -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')