language: cleanly ban integer overflows

This also bans various sneaking of negative numbers from the language
into unsuspecting builtins as was exposed while auditing the
consequences of changing the Nix language integer type to a newtype.

It's unlikely that this change comprehensively ensures correctness when
passing integers out of the Nix language and we should probably add a
checked-narrowing function or something similar, but that's out of scope
for the immediate change.

During the development of this I found a few fun facts about the
language:
- You could overflow integers by converting from unsigned JSON values.
- You could overflow unsigned integers by converting negative numbers
  into them when going into Nix config, into fetchTree, and into flake
  inputs.

  The flake inputs and Nix config cannot actually be tested properly
  since they both ban thunks, however, we put in checks anyway because
  it's possible these could somehow be used to do such shenanigans some
  other way.

Note that Lix has banned Nix language integer overflows since the very
first public beta, but threw a SIGILL about them because we run with
-fsanitize=signed-overflow -fsanitize-undefined-trap-on-error in
production builds. Since the Nix language uses signed integers, overflow
was simply undefined behaviour, and since we defined that to trap, it
did.

Trapping on it was a bad UX, but we didn't even entirely notice
that we had done this at all until it was reported as a bug a couple of
months later (which is, to be fair, that flag working as intended), and
it's got enough production time that, aside from code that is IMHO buggy
(and which is, in any case, not in nixpkgs) such as
#445, we don't think
anyone doing anything reasonable actually depends on wrapping overflow.

Even for weird use cases such as doing funny bit crimes, it doesn't make
sense IMO to have wrapping behaviour, since two's complement arithmetic
overflow behaviour is so *aggressively* not what you want for *any* kind
of mathematics/algorithms. The Nix language exists for package
management, a domain where bit crimes are already only dubiously in
scope to begin with, and it makes a lot more sense for that domain for
the integers to never lose precision, either by throwing errors if they
would, or by being arbitrary-precision.

This change will be ported to CppNix as well, to maintain language
consistency.

Fixes: #423

Change-Id: I51f253840c4af2ea5422b8a420aa5fafbf8fae75
This commit is contained in:
jade 2024-07-12 16:22:34 +02:00
parent f9641b9efd
commit 917c9bdee7
29 changed files with 254 additions and 59 deletions

View file

@ -37,7 +37,7 @@ struct ProfileElement
StorePathSet storePaths; StorePathSet storePaths;
std::optional<ProfileElementSource> source; std::optional<ProfileElementSource> source;
bool active = true; bool active = true;
int priority = DEFAULT_PRIORITY; NixInt::Inner priority = DEFAULT_PRIORITY;
std::string identifier() const; std::string identifier() const;

View file

@ -116,12 +116,12 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths()
auto drvPath = attr->forceDerivation(); auto drvPath = attr->forceDerivation();
std::optional<NixInt> priority; std::optional<NixInt::Inner> priority;
if (attr->maybeGetAttr(state->sOutputSpecified)) { if (attr->maybeGetAttr(state->sOutputSpecified)) {
} else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) { } else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) {
if (auto aPriority = aMeta->maybeGetAttr("priority")) if (auto aPriority = aMeta->maybeGetAttr("priority"))
priority = aPriority->getInt(); priority = aPriority->getInt().value;
} }
return {{ return {{

View file

@ -40,7 +40,7 @@ struct ExtraPathInfoValue : ExtraPathInfo
/** /**
* An optional priority for use with "build envs". See Package * An optional priority for use with "build envs". See Package
*/ */
std::optional<NixInt> priority; std::optional<NixInt::Inner> priority;
/** /**
* The attribute path associated with this value. The idea is * The attribute path associated with this value. The idea is

View file

@ -306,7 +306,7 @@ struct AttrDb
case AttrType::Bool: case AttrType::Bool:
return {{rowId, queryAttribute.getInt(2) != 0}}; return {{rowId, queryAttribute.getInt(2) != 0}};
case AttrType::Int: case AttrType::Int:
return {{rowId, int_t{queryAttribute.getInt(2)}}}; return {{rowId, int_t{NixInt{queryAttribute.getInt(2)}}}};
case AttrType::ListOfStrings: case AttrType::ListOfStrings:
return {{rowId, tokenizeString<std::vector<std::string>>(queryAttribute.getStr(2), "\t")}}; return {{rowId, tokenizeString<std::vector<std::string>>(queryAttribute.getStr(2), "\t")}};
case AttrType::Missing: case AttrType::Missing:
@ -449,7 +449,7 @@ Value & AttrCursor::forceValue()
else if (v.type() == nBool) else if (v.type() == nBool)
cachedValue = {root->db->setBool(getKey(), v.boolean), v.boolean}; cachedValue = {root->db->setBool(getKey(), v.boolean), v.boolean};
else if (v.type() == nInt) else if (v.type() == nInt)
cachedValue = {root->db->setInt(getKey(), v.integer), int_t{v.integer}}; cachedValue = {root->db->setInt(getKey(), v.integer.value), int_t{v.integer}};
else if (v.type() == nAttrs) else if (v.type() == nAttrs)
; // FIXME: do something? ; // FIXME: do something?
else else

View file

@ -2141,7 +2141,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
NixStringContext context; NixStringContext context;
std::vector<BackedStringView> s; std::vector<BackedStringView> s;
size_t sSize = 0; size_t sSize = 0;
NixInt n = 0; NixInt n{0};
NixFloat nf = 0; NixFloat nf = 0;
bool first = !forceString; bool first = !forceString;
@ -2185,17 +2185,22 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
if (firstType == nInt) { if (firstType == nInt) {
if (vTmp.type() == nInt) { if (vTmp.type() == nInt) {
n += vTmp.integer; auto newN = n + vTmp.integer;
if (auto checked = newN.valueChecked(); checked.has_value()) {
n = NixInt(*checked);
} else {
state.error<EvalError>("integer overflow in adding %1% + %2%", n, vTmp.integer).atPos(i_pos).debugThrow();
}
} else if (vTmp.type() == nFloat) { } else if (vTmp.type() == nFloat) {
// Upgrade the type from int to float; // Upgrade the type from int to float;
firstType = nFloat; firstType = nFloat;
nf = n; nf = n.value;
nf += vTmp.fpoint; nf += vTmp.fpoint;
} else } else
state.error<EvalError>("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow(); state.error<EvalError>("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow();
} else if (firstType == nFloat) { } else if (firstType == nFloat) {
if (vTmp.type() == nInt) { if (vTmp.type() == nInt) {
nf += vTmp.integer; nf += vTmp.integer.value;
} else if (vTmp.type() == nFloat) { } else if (vTmp.type() == nFloat) {
nf += vTmp.fpoint; nf += vTmp.fpoint;
} else } else
@ -2320,7 +2325,7 @@ NixFloat EvalState::forceFloat(Value & v, const PosIdx pos, std::string_view err
try { try {
forceValue(v, pos); forceValue(v, pos);
if (v.type() == nInt) if (v.type() == nInt)
return v.integer; return v.integer.value;
else if (v.type() != nFloat) else if (v.type() != nFloat)
error<TypeError>( error<TypeError>(
"expected a float but found %1%: %2%", "expected a float but found %1%: %2%",
@ -2507,7 +2512,7 @@ BackedStringView EvalState::coerceToString(
shell scripting convenience, just like `null'. */ shell scripting convenience, just like `null'. */
if (v.type() == nBool && v.boolean) return "1"; if (v.type() == nBool && v.boolean) return "1";
if (v.type() == nBool && !v.boolean) return ""; if (v.type() == nBool && !v.boolean) return "";
if (v.type() == nInt) return std::to_string(v.integer); if (v.type() == nInt) return std::to_string(v.integer.value);
if (v.type() == nFloat) return std::to_string(v.fpoint); if (v.type() == nFloat) return std::to_string(v.fpoint);
if (v.type() == nNull) return ""; if (v.type() == nNull) return "";
@ -2651,9 +2656,9 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
// Special case type-compatibility between float and int // Special case type-compatibility between float and int
if (v1.type() == nInt && v2.type() == nFloat) if (v1.type() == nInt && v2.type() == nFloat)
return v1.integer == v2.fpoint; return v1.integer.value == v2.fpoint;
if (v1.type() == nFloat && v2.type() == nInt) if (v1.type() == nFloat && v2.type() == nInt)
return v1.fpoint == v2.integer; return v1.fpoint == v2.integer.value;
// All other types are not compatible with each other. // All other types are not compatible with each other.
if (v1.type() != v2.type()) return false; if (v1.type() != v2.type()) return false;

View file

@ -137,9 +137,16 @@ static FlakeInput parseFlakeInput(EvalState & state,
case nBool: case nBool:
attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean }); attrs.emplace(state.symbols[attr.name], Explicit<bool> { attr.value->boolean });
break; break;
case nInt: case nInt: {
attrs.emplace(state.symbols[attr.name], (long unsigned int)attr.value->integer); auto intValue = attr.value->integer.value;
if (intValue < 0) {
state.error<EvalError>("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow();
}
uint64_t asUnsigned = intValue;
attrs.emplace(state.symbols[attr.name], asUnsigned);
break; break;
}
default: default:
state.error<TypeError>("flake input attribute '%s' is %s while a string, Boolean, or integer is expected", state.error<TypeError>("flake input attribute '%s' is %s while a string, Boolean, or integer is expected",
state.symbols[attr.name], showType(*attr.value)).debugThrow(); state.symbols[attr.name], showType(*attr.value)).debugThrow();
@ -284,7 +291,7 @@ static Flake getFlake(
else if (setting.value->type() == nInt) else if (setting.value->type() == nInt)
flake.config.settings.emplace( flake.config.settings.emplace(
state.symbols[setting.name], state.symbols[setting.name],
state.forceInt(*setting.value, setting.pos, "")); state.forceInt(*setting.value, setting.pos, "").value);
else if (setting.value->type() == nBool) else if (setting.value->type() == nBool)
flake.config.settings.emplace( flake.config.settings.emplace(
state.symbols[setting.name], state.symbols[setting.name],
@ -873,8 +880,14 @@ static void prim_flakeRefToString(
for (const auto & attr : *args[0]->attrs) { for (const auto & attr : *args[0]->attrs) {
auto t = attr.value->type(); auto t = attr.value->type();
if (t == nInt) { if (t == nInt) {
attrs.emplace(state.symbols[attr.name], auto intValue = attr.value->integer.value;
(uint64_t) attr.value->integer);
if (intValue < 0) {
state.error<EvalError>("negative value given for flake ref attr %1%: %2%", state.symbols[attr.name], intValue).atPos(pos).debugThrow();
}
uint64_t asUnsigned = intValue;
attrs.emplace(state.symbols[attr.name], asUnsigned);
} else if (t == nBool) { } else if (t == nBool) {
attrs.emplace(state.symbols[attr.name], attrs.emplace(state.symbols[attr.name],
Explicit<bool> { attr.value->boolean }); Explicit<bool> { attr.value->boolean });

View file

@ -241,8 +241,8 @@ NixInt DrvInfo::queryMetaInt(const std::string & name, NixInt def)
if (v->type() == nString) { if (v->type() == nString) {
/* Backwards compatibility with before we had support for /* Backwards compatibility with before we had support for
integer meta fields. */ integer meta fields. */
if (auto n = string2Int<NixInt>(v->string.s)) if (auto n = string2Int<NixInt::Inner>(v->string.s))
return *n; return NixInt{*n};
} }
return def; return def;
} }

View file

@ -2,6 +2,7 @@
#include "value.hh" #include "value.hh"
#include "eval.hh" #include "eval.hh"
#include <limits>
#include <variant> #include <variant>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@ -102,8 +103,12 @@ public:
return true; return true;
} }
bool number_unsigned(number_unsigned_t val) bool number_unsigned(number_unsigned_t val_)
{ {
if (val_ > std::numeric_limits<NixInt::Inner>::max()) {
throw Error("unsigned json number %1% outside of Nix integer range", val_);
}
NixInt::Inner val = val_;
rs->value(state).mkInt(val); rs->value(state).mkInt(val);
rs->add(); rs->add();
return true; return true;

View file

@ -75,6 +75,7 @@ struct ExprInt : Expr
NixInt n; NixInt n;
Value v; Value v;
ExprInt(NixInt n) : n(n) { v.mkInt(n); }; ExprInt(NixInt n) : n(n) { v.mkInt(n); };
ExprInt(NixInt::Inner n) : n(n) { v.mkInt(n); };
Value * maybeThunk(EvalState & state, Env & env) override; Value * maybeThunk(EvalState & state, Env & env) override;
COMMON_METHODS COMMON_METHODS
}; };

View file

@ -582,9 +582,9 @@ struct CompareValues
{ {
try { try {
if (v1->type() == nFloat && v2->type() == nInt) if (v1->type() == nFloat && v2->type() == nInt)
return v1->fpoint < v2->integer; return v1->fpoint < v2->integer.value;
if (v1->type() == nInt && v2->type() == nFloat) if (v1->type() == nInt && v2->type() == nFloat)
return v1->integer < v2->fpoint; return v1->integer.value < v2->fpoint;
if (v1->type() != v2->type()) if (v1->type() != v2->type())
state.error<EvalError>("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow(); state.error<EvalError>("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow();
// Allow selecting a subset of enum values // Allow selecting a subset of enum values
@ -2514,13 +2514,13 @@ static struct LazyPosAcessors {
PrimOp primop_lineOfPos{ PrimOp primop_lineOfPos{
.arity = 1, .arity = 1,
.fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) { .fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) {
v.mkInt(state.positions[PosIdx(args[0]->integer)].line); v.mkInt(state.positions[PosIdx(args[0]->integer.value)].line);
} }
}; };
PrimOp primop_columnOfPos{ PrimOp primop_columnOfPos{
.arity = 1, .arity = 1,
.fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) { .fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) {
v.mkInt(state.positions[PosIdx(args[0]->integer)].column); v.mkInt(state.positions[PosIdx(args[0]->integer.value)].column);
} }
}; };
@ -2990,7 +2990,8 @@ static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Val
/* Return the n-1'th element of a list. */ /* Return the n-1'th element of a list. */
static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
elemAt(state, pos, *args[0], state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt"), v); NixInt::Inner elem = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt").value;
elemAt(state, pos, *args[0], elem, v);
} }
static RegisterPrimOp primop_elemAt({ static RegisterPrimOp primop_elemAt({
@ -3274,7 +3275,7 @@ static RegisterPrimOp primop_all({
static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
auto len = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList"); auto len = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList").value;
if (len < 0) if (len < 0)
state.error<EvalError>("cannot create list of size %1%", len).atPos(pos).debugThrow(); state.error<EvalError>("cannot create list of size %1%", len).atPos(pos).debugThrow();
@ -3284,7 +3285,7 @@ static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Va
state.forceFunction(*args[0], noPos, "while evaluating the first argument passed to builtins.genList"); state.forceFunction(*args[0], noPos, "while evaluating the first argument passed to builtins.genList");
state.mkList(v, len); state.mkList(v, len);
for (unsigned int n = 0; n < (unsigned int) len; ++n) { for (size_t n = 0; n < len; ++n) {
auto arg = state.allocValue(); auto arg = state.allocValue();
arg->mkInt(n); arg->mkInt(n);
(v.listElems()[n] = state.allocValue())->mkApp(args[0], arg); (v.listElems()[n] = state.allocValue())->mkApp(args[0], arg);
@ -3534,9 +3535,17 @@ static void prim_add(EvalState & state, const PosIdx pos, Value * * args, Value
if (args[0]->type() == nFloat || args[1]->type() == nFloat) if (args[0]->type() == nFloat || args[1]->type() == nFloat)
v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the addition") v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the addition")
+ state.forceFloat(*args[1], pos, "while evaluating the second argument of the addition")); + state.forceFloat(*args[1], pos, "while evaluating the second argument of the addition"));
else else {
v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the addition") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument of the addition");
+ state.forceInt(*args[1], pos, "while evaluating the second argument of the addition")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument of the addition");
auto result_ = i1 + i2;
if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
} else {
state.error<EvalError>("integer overflow in adding %1% + %2%", i1, i2).atPos(pos).debugThrow();
}
}
} }
static RegisterPrimOp primop_add({ static RegisterPrimOp primop_add({
@ -3555,9 +3564,18 @@ static void prim_sub(EvalState & state, const PosIdx pos, Value * * args, Value
if (args[0]->type() == nFloat || args[1]->type() == nFloat) if (args[0]->type() == nFloat || args[1]->type() == nFloat)
v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the subtraction") v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the subtraction")
- state.forceFloat(*args[1], pos, "while evaluating the second argument of the subtraction")); - state.forceFloat(*args[1], pos, "while evaluating the second argument of the subtraction"));
else else {
v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the subtraction") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument of the subtraction");
- state.forceInt(*args[1], pos, "while evaluating the second argument of the subtraction")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument of the subtraction");
auto result_ = i1 - i2;
if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
} else {
state.error<EvalError>("integer overflow in subtracting %1% - %2%", i1, i2).atPos(pos).debugThrow();
}
}
} }
static RegisterPrimOp primop_sub({ static RegisterPrimOp primop_sub({
@ -3576,9 +3594,18 @@ static void prim_mul(EvalState & state, const PosIdx pos, Value * * args, Value
if (args[0]->type() == nFloat || args[1]->type() == nFloat) if (args[0]->type() == nFloat || args[1]->type() == nFloat)
v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first of the multiplication") v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first of the multiplication")
* state.forceFloat(*args[1], pos, "while evaluating the second argument of the multiplication")); * state.forceFloat(*args[1], pos, "while evaluating the second argument of the multiplication"));
else else {
v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the multiplication") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument of the multiplication");
* state.forceInt(*args[1], pos, "while evaluating the second argument of the multiplication")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument of the multiplication");
auto result_ = i1 * i2;
if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
} else {
state.error<EvalError>("integer overflow in multiplying %1% * %2%", i1, i2).atPos(pos).debugThrow();
}
}
} }
static RegisterPrimOp primop_mul({ static RegisterPrimOp primop_mul({
@ -3605,10 +3632,12 @@ static void prim_div(EvalState & state, const PosIdx pos, Value * * args, Value
NixInt i1 = state.forceInt(*args[0], pos, "while evaluating the first operand of the division"); NixInt i1 = state.forceInt(*args[0], pos, "while evaluating the first operand of the division");
NixInt i2 = state.forceInt(*args[1], pos, "while evaluating the second operand of the division"); NixInt i2 = state.forceInt(*args[1], pos, "while evaluating the second operand of the division");
/* Avoid division overflow as it might raise SIGFPE. */ /* Avoid division overflow as it might raise SIGFPE. */
if (i1 == std::numeric_limits<NixInt>::min() && i2 == -1) auto result_ = i1 / i2;
state.error<EvalError>("overflow in integer division").atPos(pos).debugThrow(); if (auto result = result_.valueChecked(); result.has_value()) {
v.mkInt(*result);
v.mkInt(i1 / i2); } else {
state.error<EvalError>("integer overflow in dividing %1% / %2%", i1, i2).atPos(pos).debugThrow();
}
} }
} }
@ -3623,8 +3652,9 @@ static RegisterPrimOp primop_div({
static void prim_bitAnd(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_bitAnd(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitAnd") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitAnd");
& state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitAnd")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitAnd");
v.mkInt(i1.value & i2.value);
} }
static RegisterPrimOp primop_bitAnd({ static RegisterPrimOp primop_bitAnd({
@ -3638,8 +3668,10 @@ static RegisterPrimOp primop_bitAnd({
static void prim_bitOr(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_bitOr(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitOr") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitOr");
| state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitOr")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitOr");
v.mkInt(i1.value | i2.value);
} }
static RegisterPrimOp primop_bitOr({ static RegisterPrimOp primop_bitOr({
@ -3653,8 +3685,10 @@ static RegisterPrimOp primop_bitOr({
static void prim_bitXor(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_bitXor(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitXor") auto i1 = state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitXor");
^ state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitXor")); auto i2 = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitXor");
v.mkInt(i1.value ^ i2.value);
} }
static RegisterPrimOp primop_bitXor({ static RegisterPrimOp primop_bitXor({
@ -3734,13 +3768,19 @@ static RegisterPrimOp primop_toString({
non-negative. */ non-negative. */
static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, Value & v) static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{ {
int start = state.forceInt(*args[0], pos, "while evaluating the first argument (the start offset) passed to builtins.substring"); NixInt::Inner start = state.forceInt(*args[0], pos, "while evaluating the first argument (the start offset) passed to builtins.substring").value;
if (start < 0) if (start < 0)
state.error<EvalError>("negative start position in 'substring'").atPos(pos).debugThrow(); state.error<EvalError>("negative start position in 'substring'").atPos(pos).debugThrow();
int len = state.forceInt(*args[1], pos, "while evaluating the second argument (the substring length) passed to builtins.substring"); NixInt::Inner len = state.forceInt(*args[1], pos, "while evaluating the second argument (the substring length) passed to builtins.substring").value;
// Negative length may be idiomatically passed to builtins.substring to get
// the tail of the string.
if (len < 0) {
len = std::numeric_limits<NixInt::Inner>::max();
}
// Special-case on empty substring to avoid O(n) strlen // Special-case on empty substring to avoid O(n) strlen
// This allows for the use of empty substrings to efficently capture string context // This allows for the use of empty substrings to efficently capture string context
@ -3782,7 +3822,7 @@ static void prim_stringLength(EvalState & state, const PosIdx pos, Value * * arg
{ {
NixStringContext context; NixStringContext context;
auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.stringLength"); auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.stringLength");
v.mkInt(s->size()); v.mkInt(NixInt::Inner(s->size()));
} }
static RegisterPrimOp primop_stringLength({ static RegisterPrimOp primop_stringLength({

View file

@ -148,9 +148,16 @@ static void fetchTree(
} }
else if (attr.value->type() == nBool) else if (attr.value->type() == nBool)
attrs.emplace(state.symbols[attr.name], Explicit<bool>{attr.value->boolean}); attrs.emplace(state.symbols[attr.name], Explicit<bool>{attr.value->boolean});
else if (attr.value->type() == nInt) else if (attr.value->type() == nInt) {
attrs.emplace(state.symbols[attr.name], uint64_t(attr.value->integer)); auto intValue = attr.value->integer.value;
else
if (intValue < 0) {
state.error<EvalError>("negative value given for fetchTree attr %1%: %2%", state.symbols[attr.name], intValue).atPos(pos).debugThrow();
}
unsigned long asUnsigned = intValue;
attrs.emplace(state.symbols[attr.name], asUnsigned);
} else
state.error<TypeError>("fetchTree argument '%s' is %s while a string, Boolean or integer is expected", state.error<TypeError>("fetchTree argument '%s' is %s while a string, Boolean or integer is expected",
state.symbols[attr.name], showType(*attr.value)).debugThrow(); state.symbols[attr.name], showType(*attr.value)).debugThrow();
} }

View file

@ -22,7 +22,7 @@ json printValueAsJSON(EvalState & state, bool strict,
switch (v.type()) { switch (v.type()) {
case nInt: case nInt:
out = v.integer; out = v.integer.value;
break; break;
case nBool: case nBool:

View file

@ -9,6 +9,7 @@
#include "input-accessor.hh" #include "input-accessor.hh"
#include "source-path.hh" #include "source-path.hh"
#include "print-options.hh" #include "print-options.hh"
#include "checked-arithmetic.hh"
#if HAVE_BOEHMGC #if HAVE_BOEHMGC
#include <gc/gc_allocator.h> #include <gc/gc_allocator.h>
@ -73,8 +74,8 @@ class EvalState;
class XMLWriter; class XMLWriter;
class Printer; class Printer;
typedef int64_t NixInt; using NixInt = checked::Checked<int64_t>;
typedef double NixFloat; using NixFloat = double;
/** /**
* External values must descend from ExternalValueBase, so that * External values must descend from ExternalValueBase, so that
@ -254,6 +255,11 @@ public:
app.left = app.right = 0; app.left = app.right = 0;
} }
inline void mkInt(NixInt::Inner n)
{
mkInt(NixInt{n});
}
inline void mkInt(NixInt n) inline void mkInt(NixInt n)
{ {
clearValue(); clearValue();

View file

@ -981,7 +981,10 @@ public:
)"}; )"};
Setting<uint64_t> maxFree{ Setting<uint64_t> maxFree{
this, std::numeric_limits<uint64_t>::max(), "max-free", // n.b. this is deliberately int64 max rather than uint64 max because
// this goes through the Nix language JSON parser and thus needs to be
// representable in Nix language integers.
this, std::numeric_limits<int64_t>::max(), "max-free",
R"( R"(
When a garbage collection is triggered by the `min-free` option, it When a garbage collection is triggered by the `min-free` option, it
stops as soon as `max-free` bytes are available. The default is stops as soon as `max-free` bytes are available. The default is

View file

@ -0,0 +1,8 @@
error:
… while calling the 'fetchTree' builtin
at /pwd/lang/eval-fail-fetchTree-negative.nix:1:1:
1| builtins.fetchTree {
| ^
2| type = "file";
error: negative value given for fetchTree attr owner: -1

View file

@ -0,0 +1,5 @@
builtins.fetchTree {
type = "file";
url = "file://eval-fail-fetchTree-negative.nix";
owner = -1;
}

View file

@ -0,0 +1,14 @@
error:
… while calling the 'seq' builtin
at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:1:16:
1| let n = -1; in builtins.seq n (builtins.flakeRefToString {
| ^
2| type = "github";
… while calling the 'flakeRefToString' builtin
at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:1:32:
1| let n = -1; in builtins.seq n (builtins.flakeRefToString {
| ^
2| type = "github";
error: negative value given for flake ref attr repo: -1

View file

@ -0,0 +1,7 @@
let n = -1; in builtins.seq n (builtins.flakeRefToString {
type = "github";
owner = "NixOS";
repo = n;
ref = "23.05";
dir = "lib";
})

View file

@ -0,0 +1,8 @@
error:
… while calling the 'fromJSON' builtin
at /pwd/lang/eval-fail-fromJSON-overflowing.nix:1:1:
1| builtins.fromJSON ''{"attr": 18446744073709551615}''
| ^
2|
error: unsigned json number 18446744073709551615 outside of Nix integer range

View file

@ -0,0 +1 @@
builtins.fromJSON ''{"attr": 18446744073709551615}''

View file

@ -0,0 +1,6 @@
error: integer overflow in adding 9223372036854775807 + 1
at /pwd/lang/eval-fail-overflowing-add.nix:4:8:
3| b = 1;
4| in a + b
| ^
5|

View file

@ -0,0 +1,4 @@
let
a = 9223372036854775807;
b = 1;
in a + b

View file

@ -0,0 +1,23 @@
error:
… while calling the 'seq' builtin
at /pwd/lang/eval-fail-overflowing-div.nix:7:4:
6| b = -1;
7| in builtins.seq intMin (builtins.seq b (intMin / b))
| ^
8|
… while calling the 'seq' builtin
at /pwd/lang/eval-fail-overflowing-div.nix:7:25:
6| b = -1;
7| in builtins.seq intMin (builtins.seq b (intMin / b))
| ^
8|
… while calling the 'div' builtin
at /pwd/lang/eval-fail-overflowing-div.nix:7:48:
6| b = -1;
7| in builtins.seq intMin (builtins.seq b (intMin / b))
| ^
8|
error: integer overflow in dividing -9223372036854775808 / -1

View file

@ -0,0 +1,7 @@
let
# lol, this has to be written as an expression like this because negative
# numbers use unary negation rather than parsing directly, and 2**63 is out
# of range
intMin = -9223372036854775807 - 1;
b = -1;
in builtins.seq intMin (builtins.seq b (intMin / b))

View file

@ -0,0 +1,16 @@
error:
… while calling the 'mul' builtin
at /pwd/lang/eval-fail-overflowing-mul.nix:3:10:
2| a = 4294967297;
3| in a * a * a
| ^
4|
… while calling the 'mul' builtin
at /pwd/lang/eval-fail-overflowing-mul.nix:3:6:
2| a = 4294967297;
3| in a * a * a
| ^
4|
error: integer overflow in multiplying 4294967297 * 4294967297

View file

@ -0,0 +1,3 @@
let
a = 4294967297;
in a * a * a

View file

@ -0,0 +1,9 @@
error:
… while calling the 'sub' builtin
at /pwd/lang/eval-fail-overflowing-sub.nix:4:6:
3| b = 2;
4| in a - b
| ^
5|
error: integer overflow in subtracting -9223372036854775807 - 2

View file

@ -0,0 +1,4 @@
let
a = -9223372036854775807;
b = 2;
in a - b

View file

@ -77,7 +77,7 @@ namespace nix {
if (arg.type() != nInt) { if (arg.type() != nInt) {
return false; return false;
} }
return arg.integer == v; return arg.integer.value == v;
} }
MATCHER_P(IsFloatEq, v, fmt("The float is equal to \"%1%\"", v)) { MATCHER_P(IsFloatEq, v, fmt("The float is equal to \"%1%\"", v)) {