Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8a447db88 | ||
|
|
ec912eda76 | ||
|
|
667b40f1ff | ||
|
|
64599c010c | ||
|
|
1953c4ae4b | ||
|
|
7e56afa0b1 | ||
|
|
5d6134064e | ||
|
|
1d36c544be | ||
|
|
7b210490aa | ||
|
|
fba0fa5767 | ||
|
|
533429d89a | ||
|
|
c135090468 | ||
|
|
72f7965679 | ||
|
|
b7be40c785 | ||
|
|
6350f51458 | ||
|
|
c6f3f3a0d3 | ||
|
|
de4cfec46a | ||
|
|
0873bed39d | ||
|
|
5dcb90548f |
@@ -73,6 +73,9 @@ detroyejr:
|
|||||||
display_name: Jonathan De Troye
|
display_name: Jonathan De Troye
|
||||||
github: detroyejr
|
github: detroyejr
|
||||||
|
|
||||||
|
edef:
|
||||||
|
github: edef1c
|
||||||
|
|
||||||
edolstra:
|
edolstra:
|
||||||
display_name: Eelco Dolstra
|
display_name: Eelco Dolstra
|
||||||
github: edolstra
|
github: edolstra
|
||||||
@@ -229,6 +232,9 @@ roberth:
|
|||||||
display_name: Robert Hensing
|
display_name: Robert Hensing
|
||||||
github: roberth
|
github: roberth
|
||||||
|
|
||||||
|
sandydoo:
|
||||||
|
github: sandydoo
|
||||||
|
|
||||||
seppel3210:
|
seppel3210:
|
||||||
github: Seppel3210
|
github: Seppel3210
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,38 @@
|
|||||||
# Lix 2.94 "Açaí na tigela" (2025-11-17)
|
# Lix 2.94 "Açaí na tigela" (2025-11-17)
|
||||||
|
# Lix 2.94.2 (2026-05-04)
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fix unsigned overflow leading to out-of-band write in the NAR parser [cl/5553](https://gerrit.lix.systems/c/lix/+/5553)
|
||||||
|
|
||||||
|
The NAR parser contained an unsigned integer overflow that could be used by an
|
||||||
|
attacker to write arbitrary data to an unknown memory location and possibly
|
||||||
|
achieve code execution. A successful attack on the system-wide Lix daemon
|
||||||
|
could lead to privilege escalation to root. Any process that involves NAR
|
||||||
|
serialization could trigger this issue, including (but not limited to)
|
||||||
|
|
||||||
|
- local user interaction, whether the users are trusted or untrusted
|
||||||
|
- malicious substituters sending malformed NARs
|
||||||
|
- remote builders sending malformed build results
|
||||||
|
- remote daemons sending malformed inputs when requesting remote builds
|
||||||
|
|
||||||
|
Successful attacks using this bug require ASLR weakening of some sort, whether
|
||||||
|
by architecture constraints (e.g. on 32 bit systems, where little randomization
|
||||||
|
is possible) or system configuration (e.g. low ASLR entropy when loading
|
||||||
|
libraries), and millions of attempts. Local attacks can be mounted in less than
|
||||||
|
an hour. Remote builds typically require a fresh SSH connection for each build
|
||||||
|
and are thus less susceptible. Only one attempt can be made by substituters for
|
||||||
|
every build using substituters, they are thus not a likely vector for attacks.
|
||||||
|
|
||||||
|
At the time of writing, MITRE has not assigned this a CVE yet.
|
||||||
|
|
||||||
|
Many thanks to [eldritch horrors](https://git.lix.systems/pennae), [Raito Bezarius](https://git.lix.systems/raito), [edef](https://github.com/edef1c), and [sandydoo](https://github.com/sandydoo) for this.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Lix 2.94.1 (2026-03-13)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Lix 2.94.0 (2025-11-17)
|
# Lix 2.94.0 (2025-11-17)
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ std::string renderMarkdownToTerminal(std::string_view markdown, StandardOutputSt
|
|||||||
.vmargin = 0,
|
.vmargin = 0,
|
||||||
#endif /* LOWDOWN_SEPARATE_TERM_OPTS */
|
#endif /* LOWDOWN_SEPARATE_TERM_OPTS */
|
||||||
.feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES,
|
.feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES,
|
||||||
|
#ifdef LOWDOWN_CONSOLIDATED_OFLAGS
|
||||||
|
.oflags = LOWDOWN_NOLINK,
|
||||||
|
#else
|
||||||
.oflags = LOWDOWN_TERM_NOLINK,
|
.oflags = LOWDOWN_TERM_NOLINK,
|
||||||
|
#endif /* LOWDOWN_CONSOLIDATED_OFLAGS */
|
||||||
};
|
};
|
||||||
if (!shouldANSI(fileno)) {
|
if (!shouldANSI(fileno)) {
|
||||||
opts.oflags |= LOWDOWN_TERM_NOANSI;
|
opts.oflags |= LOWDOWN_TERM_NOANSI;
|
||||||
|
|||||||
@@ -20,11 +20,21 @@ inline Value::Value(app_t, EvalMemory & mem, Value & lhs, Value & rhs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline Value::Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value> args)
|
inline Value::Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value> args)
|
||||||
|
: Value(app_t{}, mem, lhs, args, {})
|
||||||
{
|
{
|
||||||
auto app = static_cast<Value::App *>(mem.allocBytes(sizeof(Value::App) + args.size_bytes()));
|
}
|
||||||
|
|
||||||
|
inline Value::Value(
|
||||||
|
app_t, EvalMemory & mem, const Value & lhs, std::span<Value> baseArgs, std::span<Value> moreArgs
|
||||||
|
)
|
||||||
|
{
|
||||||
|
auto app = static_cast<Value::App *>(
|
||||||
|
mem.allocBytes(sizeof(Value::App) + baseArgs.size_bytes() + moreArgs.size_bytes())
|
||||||
|
);
|
||||||
app->_left = lhs;
|
app->_left = lhs;
|
||||||
app->_n = args.size();
|
app->_n = baseArgs.size() + moreArgs.size();
|
||||||
std::copy(args.begin(), args.end(), app->_args);
|
std::copy(baseArgs.begin(), baseArgs.end(), app->_args);
|
||||||
|
std::copy(moreArgs.begin(), moreArgs.end(), app->_args + baseArgs.size());
|
||||||
raw = tag(tApp, app);
|
raw = tag(tApp, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -1639,7 +1639,14 @@ void EvalState::callFunction(Value & fun, std::span<Value> args, Value & vRes, c
|
|||||||
|
|
||||||
Value vCur(fun);
|
Value vCur(fun);
|
||||||
|
|
||||||
auto makeAppChain = [&]() { vRes = {NewValueAs::app, ctx.mem, vCur, args}; };
|
auto makeAppChain = [&]() {
|
||||||
|
if (vCur.isApp()) {
|
||||||
|
auto & app = vCur.app();
|
||||||
|
vRes = {NewValueAs::app, ctx.mem, app.left(), app.args(), args};
|
||||||
|
} else {
|
||||||
|
vRes = {NewValueAs::app, ctx.mem, vCur, args};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Attr * functor;
|
const Attr * functor;
|
||||||
|
|
||||||
|
|||||||
@@ -544,6 +544,10 @@ public:
|
|||||||
/// lazy and/or partial application of a function.
|
/// lazy and/or partial application of a function.
|
||||||
Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value> args);
|
Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value> args);
|
||||||
|
|
||||||
|
/// Constructs a nix language value of type "lambda", which represents a
|
||||||
|
/// lazy and/or partial application of a function.
|
||||||
|
Value(app_t, EvalMemory & mem, const Value & lhs, std::span<Value> baseArgs, std::span<Value> moreArgs);
|
||||||
|
|
||||||
/// Constructs a nix language value of type "external", which is only used
|
/// Constructs a nix language value of type "external", which is only used
|
||||||
/// by plugins. Do any existing plugins even use this mechanism?
|
/// by plugins. Do any existing plugins even use this mechanism?
|
||||||
Value(external_t, ExternalValueBase & external)
|
Value(external_t, ExternalValueBase & external)
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ try {
|
|||||||
|
|
||||||
auto * buildIdDir = std::get_if<nar_index::Directory>(&narIndex);
|
auto * buildIdDir = std::get_if<nar_index::Directory>(&narIndex);
|
||||||
for (auto subdir : { "lib", "debug", ".build-id" }) {
|
for (auto subdir : { "lib", "debug", ".build-id" }) {
|
||||||
|
if (!buildIdDir) break;
|
||||||
// get returns nullptr subdir does not exist, and std::get_if propagates it.
|
// get returns nullptr subdir does not exist, and std::get_if propagates it.
|
||||||
buildIdDir = std::get_if<nar_index::Directory>(get(buildIdDir->contents, subdir));
|
buildIdDir = std::get_if<nar_index::Directory>(get(buildIdDir->contents, subdir));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,14 @@ Goal::WorkResult DerivationGoal::timedOut(Error && ex)
|
|||||||
|
|
||||||
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::workImpl() noexcept
|
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::workImpl() noexcept
|
||||||
{
|
{
|
||||||
KJ_DEFER({ actLock.reset(); });
|
// always clear the slot token, no matter what happens. not doing this
|
||||||
|
// can cause builds to get stuck on exceptions (or other early exits).
|
||||||
|
// ideally we'd use scoped slot tokens instead of keeping them in some
|
||||||
|
// goal member variable, but we cannot do this yet for legacy reasons.
|
||||||
|
KJ_DEFER({
|
||||||
|
actLock.reset();
|
||||||
|
slotToken = {};
|
||||||
|
});
|
||||||
|
|
||||||
BOOST_OUTCOME_CO_TRY(auto result, co_await (useDerivation ? getDerivation() : haveDerivation()));
|
BOOST_OUTCOME_CO_TRY(auto result, co_await (useDerivation ? getDerivation() : haveDerivation()));
|
||||||
result.storePath = drvPath;
|
result.storePath = drvPath;
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ struct DerivationGoal : public Goal
|
|||||||
|
|
||||||
WorkResult timedOut(Error && ex);
|
WorkResult timedOut(Error && ex);
|
||||||
|
|
||||||
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
|
kj::Promise<Result<WorkResult>> workImpl() noexcept override final;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add wanted outputs to an already existing derivation goal.
|
* Add wanted outputs to an already existing derivation goal.
|
||||||
@@ -307,6 +307,8 @@ struct DerivationGoal : public Goal
|
|||||||
virtual void cleanupPostOutputsRegisteredModeNonCheck();
|
virtual void cleanupPostOutputsRegisteredModeNonCheck();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
AsyncSemaphore::Token slotToken;
|
||||||
|
|
||||||
kj::TimePoint lastChildActivity = kj::minValue;
|
kj::TimePoint lastChildActivity = kj::minValue;
|
||||||
|
|
||||||
kj::Promise<Result<WorkResult>> wrapChildHandler(kj::Promise<Result<WorkResult>> handler
|
kj::Promise<Result<WorkResult>> wrapChildHandler(kj::Promise<Result<WorkResult>> handler
|
||||||
|
|||||||
@@ -22,12 +22,6 @@ kj::Promise<void> Goal::waitForAWhile()
|
|||||||
|
|
||||||
kj::Promise<Result<Goal::WorkResult>> Goal::work() noexcept
|
kj::Promise<Result<Goal::WorkResult>> Goal::work() noexcept
|
||||||
try {
|
try {
|
||||||
// always clear the slot token, no matter what happens. not doing this
|
|
||||||
// can cause builds to get stuck on exceptions (or other early exist).
|
|
||||||
// ideally we'd use scoped slot tokens instead of keeping them in some
|
|
||||||
// goal member variable, but we cannot do this yet for legacy reasons.
|
|
||||||
KJ_DEFER({ slotToken = {}; });
|
|
||||||
|
|
||||||
BOOST_OUTCOME_CO_TRY(auto result, co_await workImpl());
|
BOOST_OUTCOME_CO_TRY(auto result, co_await workImpl());
|
||||||
|
|
||||||
trace("done");
|
trace("done");
|
||||||
|
|||||||
@@ -82,9 +82,6 @@ struct Goal
|
|||||||
*/
|
*/
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|
||||||
protected:
|
|
||||||
AsyncSemaphore::Token slotToken;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
struct [[nodiscard]] WorkResult {
|
struct [[nodiscard]] WorkResult {
|
||||||
ExitCode exitCode;
|
ExitCode exitCode;
|
||||||
|
|||||||
@@ -1825,15 +1825,18 @@ try {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputGraph[scratchOutputs.at(name)] = StorePathSet{};
|
||||||
std::visit(
|
std::visit(
|
||||||
overloaded{/* Since we'll use the already installed versions of these, we
|
overloaded{/* Since we'll use the already installed versions of these, we
|
||||||
can treat them as leaves and ignore any references they
|
can treat them as leaves and ignore any references they
|
||||||
have. */
|
have. */
|
||||||
[&](const AlreadyRegistered &) {
|
[&](const AlreadyRegistered &) {},
|
||||||
outputGraph[scratchOutputs.at(name)] = StorePathSet{};
|
|
||||||
},
|
|
||||||
[&](const PerhapsNeedToRegister & refs) {
|
[&](const PerhapsNeedToRegister & refs) {
|
||||||
outputGraph[scratchOutputs.at(name)] = refs.refs;
|
for (auto & ref : refs.refs) {
|
||||||
|
if (inverseOutputMap.find(ref) != inverseOutputMap.end()) {
|
||||||
|
outputGraph[scratchOutputs.at(name)].insert(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
*orifu
|
*orifu
|
||||||
@@ -1844,10 +1847,8 @@ try {
|
|||||||
topoSort(outputsToSort, {[&](const std::string & name) {
|
topoSort(outputsToSort, {[&](const std::string & name) {
|
||||||
StringSet dependencies;
|
StringSet dependencies;
|
||||||
for (auto & path : outputGraph.at(scratchOutputs.at(name))) {
|
for (auto & path : outputGraph.at(scratchOutputs.at(name))) {
|
||||||
auto outputName = inverseOutputMap.find(path);
|
auto outputName = inverseOutputMap.at(path);
|
||||||
if (outputName != inverseOutputMap.end()) {
|
dependencies.insert(outputName);
|
||||||
dependencies.insert(outputName->second);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dependencies;
|
return dependencies;
|
||||||
}});
|
}});
|
||||||
|
|||||||
@@ -27,13 +27,6 @@ PathSubstitutionGoal::PathSubstitutionGoal(
|
|||||||
maintainExpectedSubstitutions = worker.expectedSubstitutions.addTemporarily(1);
|
maintainExpectedSubstitutions = worker.expectedSubstitutions.addTemporarily(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
PathSubstitutionGoal::~PathSubstitutionGoal()
|
|
||||||
{
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Goal::WorkResult PathSubstitutionGoal::done(
|
Goal::WorkResult PathSubstitutionGoal::done(
|
||||||
ExitCode result,
|
ExitCode result,
|
||||||
BuildResult::Status status,
|
BuildResult::Status status,
|
||||||
@@ -76,8 +69,6 @@ kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryNext() noexcept
|
|||||||
try {
|
try {
|
||||||
trace("trying next substituter");
|
trace("trying next substituter");
|
||||||
|
|
||||||
cleanup();
|
|
||||||
|
|
||||||
if (subs.size() == 0) {
|
if (subs.size() == 0) {
|
||||||
/* None left. Terminate this goal and let someone else deal
|
/* None left. Terminate this goal and let someone else deal
|
||||||
with it. */
|
with it. */
|
||||||
@@ -205,29 +196,20 @@ kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryToRun() noexcept
|
|||||||
try {
|
try {
|
||||||
trace("trying to run");
|
trace("trying to run");
|
||||||
|
|
||||||
if (!slotToken.valid()) {
|
auto & fetchPath = subPath ? *subPath : storePath;
|
||||||
slotToken = co_await worker.substitutions.acquire();
|
do {
|
||||||
}
|
try {
|
||||||
|
try {
|
||||||
|
AsyncSemaphore::Token slotToken = co_await worker.substitutions.acquire();
|
||||||
|
|
||||||
|
auto act = logger->startActivity(
|
||||||
|
actSubstitute,
|
||||||
|
Logger::Fields{worker.store.printStorePath(storePath), sub->getUri()}
|
||||||
|
);
|
||||||
|
|
||||||
maintainRunningSubstitutions = worker.runningSubstitutions.addTemporarily(1);
|
maintainRunningSubstitutions = worker.runningSubstitutions.addTemporarily(1);
|
||||||
|
|
||||||
auto pipe = kj::newPromiseAndCrossThreadFulfiller<void>();
|
TRY_AWAIT(copyStorePath(
|
||||||
outPipe = kj::mv(pipe.fulfiller);
|
|
||||||
|
|
||||||
thr = std::async(std::launch::async, [this]() {
|
|
||||||
AsyncIoRoot aio;
|
|
||||||
/* Wake up the worker loop when we're done. */
|
|
||||||
Finally updateStats([this]() { outPipe->fulfill(); });
|
|
||||||
|
|
||||||
auto & fetchPath = subPath ? *subPath : storePath;
|
|
||||||
try {
|
|
||||||
ReceiveInterrupts receiveInterrupts;
|
|
||||||
|
|
||||||
auto act = logger->startActivity(
|
|
||||||
actSubstitute, Logger::Fields{worker.store.printStorePath(storePath), sub->getUri()}
|
|
||||||
);
|
|
||||||
|
|
||||||
aio.blockOn(copyStorePath(
|
|
||||||
*sub,
|
*sub,
|
||||||
worker.store,
|
worker.store,
|
||||||
fetchPath,
|
fetchPath,
|
||||||
@@ -235,6 +217,8 @@ try {
|
|||||||
sub->config().isTrusted ? NoCheckSigs : CheckSigs,
|
sub->config().isTrusted ? NoCheckSigs : CheckSigs,
|
||||||
&act
|
&act
|
||||||
));
|
));
|
||||||
|
|
||||||
|
break;
|
||||||
} catch (const EndOfFile &) {
|
} catch (const EndOfFile &) {
|
||||||
throw EndOfFile(
|
throw EndOfFile(
|
||||||
"NAR for '%s' fetched from '%s' is incomplete",
|
"NAR for '%s' fetched from '%s' is incomplete",
|
||||||
@@ -242,24 +226,6 @@ try {
|
|||||||
sub->getUri()
|
sub->getUri()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
co_await pipe.promise;
|
|
||||||
co_return co_await finished();
|
|
||||||
} catch (...) {
|
|
||||||
co_return result::current_exception();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::finished() noexcept
|
|
||||||
try {
|
|
||||||
trace("substitute finished");
|
|
||||||
|
|
||||||
do {
|
|
||||||
try {
|
|
||||||
slotToken = {};
|
|
||||||
thr.get();
|
|
||||||
break;
|
|
||||||
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
|
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
|
||||||
printError("%1%", Uncolored(e.what()));
|
printError("%1%", Uncolored(e.what()));
|
||||||
|
|
||||||
@@ -271,10 +237,20 @@ try {
|
|||||||
substituterFailed = true;
|
substituterFailed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Try the next substitute. */
|
|
||||||
|
/* Try the next substitute */
|
||||||
co_return co_await tryNext();
|
co_return co_await tryNext();
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
|
co_return co_await finished();
|
||||||
|
} catch (...) {
|
||||||
|
co_return result::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::finished() noexcept
|
||||||
|
try {
|
||||||
|
trace("substitute finished");
|
||||||
|
|
||||||
worker.markContentsGood(storePath);
|
worker.markContentsGood(storePath);
|
||||||
|
|
||||||
printMsg(lvlChatty, "substitution of path '%s' succeeded", worker.store.printStorePath(storePath));
|
printMsg(lvlChatty, "substitution of path '%s' succeeded", worker.store.printStorePath(storePath));
|
||||||
@@ -294,19 +270,4 @@ try {
|
|||||||
} catch (...) {
|
} catch (...) {
|
||||||
co_return result::current_exception();
|
co_return result::current_exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PathSubstitutionGoal::cleanup()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (thr.valid()) {
|
|
||||||
// FIXME: signal worker thread to quit.
|
|
||||||
thr.get();
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
ignoreExceptionInDestructor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,11 +48,6 @@ struct PathSubstitutionGoal : public Goal
|
|||||||
*/
|
*/
|
||||||
kj::Own<kj::CrossThreadPromiseFulfiller<void>> outPipe;
|
kj::Own<kj::CrossThreadPromiseFulfiller<void>> outPipe;
|
||||||
|
|
||||||
/**
|
|
||||||
* The substituter thread.
|
|
||||||
*/
|
|
||||||
std::future<void> thr;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to try to repair a valid path.
|
* Whether to try to repair a valid path.
|
||||||
*/
|
*/
|
||||||
@@ -85,7 +80,6 @@ public:
|
|||||||
RepairFlag repair = NoRepair,
|
RepairFlag repair = NoRepair,
|
||||||
std::optional<ContentAddress> ca = std::nullopt
|
std::optional<ContentAddress> ca = std::nullopt
|
||||||
);
|
);
|
||||||
~PathSubstitutionGoal();
|
|
||||||
|
|
||||||
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
|
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
|
||||||
|
|
||||||
@@ -97,9 +91,6 @@ public:
|
|||||||
kj::Promise<Result<WorkResult>> tryToRun() noexcept;
|
kj::Promise<Result<WorkResult>> tryToRun() noexcept;
|
||||||
kj::Promise<Result<WorkResult>> finished() noexcept;
|
kj::Promise<Result<WorkResult>> finished() noexcept;
|
||||||
|
|
||||||
/* Called by destructor, can't be overridden */
|
|
||||||
void cleanup() override final;
|
|
||||||
|
|
||||||
JobCategory jobCategory() const override {
|
JobCategory jobCategory() const override {
|
||||||
return JobCategory::Substitution;
|
return JobCategory::Substitution;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -471,6 +471,6 @@ configure_file(
|
|||||||
'libdir' : libdir,
|
'libdir' : libdir,
|
||||||
'includedir' : includedir,
|
'includedir' : includedir,
|
||||||
'PACKAGE_VERSION' : meson.project_version(),
|
'PACKAGE_VERSION' : meson.project_version(),
|
||||||
'AWS_SDK_IF_FOUND' : aws_sdk.found() ? 'aws-cpp-sdk-core aws-cpp-sdk-s3 aws-cpp-std-transfer' : '',
|
'AWS_SDK_IF_FOUND' : aws_sdk.found() ? 'aws-cpp-sdk-core aws-cpp-sdk-s3 aws-cpp-sdk-transfer' : '',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ static std::map<StorePath, Node> mkGraph(
|
|||||||
|
|
||||||
for (auto & node : graph_data) {
|
for (auto & node : graph_data) {
|
||||||
for (auto & ref : node.second.dependencies) {
|
for (auto & ref : node.second.dependencies) {
|
||||||
graph_data.find(ref)->second.dependents.insert(node.first);
|
graph_data.at(ref).dependents.insert(node.first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ struct Parser
|
|||||||
buffer.clear(); \
|
buffer.clear(); \
|
||||||
std::move(str); \
|
std::move(str); \
|
||||||
})
|
})
|
||||||
#define READ_STRING() READ_STRING_LIMITED(std::numeric_limits<size_t>::max())
|
#define READ_STRING() READ_STRING_LIMITED(1048576)
|
||||||
#define READ_PADDING(size) \
|
#define READ_PADDING(size) \
|
||||||
do { \
|
do { \
|
||||||
if ((size) % 8) { \
|
if ((size) % 8) { \
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
# Cursed, but I don't think there's another way to get this environment variable.
|
||||||
|
lix_suffix = run_command('bash', '-c', 'echo -n "$VERSION_SUFFIX"', check : true).stdout().strip()
|
||||||
|
lix_version_parts = meson.project_version().split('.')
|
||||||
|
lix_major = lix_version_parts[0]
|
||||||
|
lix_minor = lix_version_parts[1]
|
||||||
|
lix_patch = lix_version_parts[2].replace(lix_suffix, '')
|
||||||
|
|
||||||
config_h = configure_file(
|
config_h = configure_file(
|
||||||
configuration : {
|
configuration : {
|
||||||
'PACKAGE_NAME': '"' + meson.project_name() + '"',
|
'PACKAGE_NAME': '"' + meson.project_name() + '"',
|
||||||
'PACKAGE_VERSION': '"' + meson.project_version() + '"',
|
'PACKAGE_VERSION': '"' + meson.project_version() + '"',
|
||||||
|
'LIX_MAJOR': lix_major,
|
||||||
|
'LIX_MINOR': lix_minor,
|
||||||
|
'LIX_PATCH': lix_patch,
|
||||||
'PACKAGE_TARNAME': '"' + meson.project_name() + '"',
|
'PACKAGE_TARNAME': '"' + meson.project_name() + '"',
|
||||||
'PACKAGE_STRING': '"' + meson.project_name() + ' ' + meson.project_version() + '"',
|
'PACKAGE_STRING': '"' + meson.project_name() + ' ' + meson.project_version() + '"',
|
||||||
'HAVE_STRUCT_DIRENT_D_TYPE': 1, # FIXME: actually check this for solaris
|
'HAVE_STRUCT_DIRENT_D_TYPE': 1, # FIXME: actually check this for solaris
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE(Raito): we override the store creation
|
||||||
|
// to prevent any store daemon connection.
|
||||||
|
//
|
||||||
|
// An upgrade, by nature, requires a direct store access
|
||||||
|
// to avoid having the daemon die in the middle of changing the binary.
|
||||||
|
//
|
||||||
|
// If more commands needs that, we can move it into a mixin. This was deliberately not done
|
||||||
|
// here.
|
||||||
|
virtual ref<Store> createStore(AsyncIoRoot & aio) override
|
||||||
|
{
|
||||||
|
return aio.blockOn(openStore(settings.storeUri.get(), {}, AllowDaemon::Disallow));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This command is stable before the others
|
* This command is stable before the others
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -350,6 +350,11 @@ if lowdown.version().version_compare('>= 1.4.0')
|
|||||||
add_project_arguments('-DLOWDOWN_SEPARATE_TERM_OPTS', language: 'cpp')
|
add_project_arguments('-DLOWDOWN_SEPARATE_TERM_OPTS', language: 'cpp')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# TODO(sterni): drop the corresponding #ifdef after NixOS 25.11 is EOL which still distributes lowdown < 3.0.0
|
||||||
|
if lowdown.version().version_compare('>= 3.0.0')
|
||||||
|
add_project_arguments('-DLOWDOWN_CONSOLIDATED_OFLAGS', language: 'cpp')
|
||||||
|
endif
|
||||||
|
|
||||||
# HACK(Qyriad): rapidcheck's pkg-config doesn't include the libs lol
|
# HACK(Qyriad): rapidcheck's pkg-config doesn't include the libs lol
|
||||||
# Note: technically we 'check' for rapidcheck twice, for the internal-api-docs handling above,
|
# Note: technically we 'check' for rapidcheck twice, for the internal-api-docs handling above,
|
||||||
# but Meson will cache the result of the first one, and the required : arguments are different.
|
# but Meson will cache the result of the first one, and the required : arguments are different.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def setup_creds(env: RelengEnvironment):
|
|||||||
key = keys.get_ephemeral_key(env)
|
key = keys.get_ephemeral_key(env)
|
||||||
$AWS_SECRET_ACCESS_KEY = key.secret_key
|
$AWS_SECRET_ACCESS_KEY = key.secret_key
|
||||||
$AWS_ACCESS_KEY_ID = key.id
|
$AWS_ACCESS_KEY_ID = key.id
|
||||||
$AWS_DEFAULT_REGION = 'garage'
|
$AWS_DEFAULT_REGION = env.s3_region
|
||||||
$AWS_ENDPOINT_URL = env.s3_endpoint
|
$AWS_ENDPOINT_URL = env.s3_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-11
@@ -5,11 +5,12 @@ import functools
|
|||||||
import subprocess
|
import subprocess
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
|
||||||
S3_HOST = 's3.lix.systems'
|
S3_HOST = 's3-admin.afnix.fr'
|
||||||
|
S3_USER = 'lix-releng'
|
||||||
|
|
||||||
DEFAULT_STORE_URI_BITS = {
|
DEFAULT_STORE_URI_BITS = {
|
||||||
'region': 'garage',
|
'region': 'global',
|
||||||
'endpoint': 's3.lix.systems',
|
'endpoint': 's3.afnix.fr',
|
||||||
'want-mass-query': 'true',
|
'want-mass-query': 'true',
|
||||||
'write-nar-listing': 'true',
|
'write-nar-listing': 'true',
|
||||||
'ls-compression': 'zstd',
|
'ls-compression': 'zstd',
|
||||||
@@ -54,7 +55,9 @@ class RelengEnvironment:
|
|||||||
git_repo: Callable[[], str]
|
git_repo: Callable[[], str]
|
||||||
git_repo_is_gerrit: bool
|
git_repo_is_gerrit: bool
|
||||||
s3_endpoint: str
|
s3_endpoint: str
|
||||||
|
s3_region: str
|
||||||
s3_ssh_host: str | None
|
s3_ssh_host: str | None
|
||||||
|
s3_ssh_user: str | None
|
||||||
|
|
||||||
docker_targets: list[DockerTarget]
|
docker_targets: list[DockerTarget]
|
||||||
|
|
||||||
@@ -86,17 +89,19 @@ LOCAL = RelengEnvironment(
|
|||||||
git_repo_is_gerrit=False,
|
git_repo_is_gerrit=False,
|
||||||
docker_targets=[],
|
docker_targets=[],
|
||||||
s3_endpoint = 'http://localhost:3900',
|
s3_endpoint = 'http://localhost:3900',
|
||||||
|
s3_region = 'garage',
|
||||||
s3_ssh_host = None,
|
s3_ssh_host = None,
|
||||||
|
s3_ssh_user = None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
STAGING = RelengEnvironment(
|
STAGING = RelengEnvironment(
|
||||||
name='staging',
|
name='staging',
|
||||||
colour=functools.partial(sgr, GREEN),
|
colour=functools.partial(sgr, GREEN),
|
||||||
docs_bucket='s3://staging-docs',
|
docs_bucket='s3://docs.staging.lix.systems',
|
||||||
cache_bucket='s3://staging-cache',
|
cache_bucket='s3://cache.staging.lix.systems',
|
||||||
cache_store_overlay={'secret-key': 'staging.key'},
|
cache_store_overlay={'secret-key': 'staging.key'},
|
||||||
releases_bucket='s3://staging-releases',
|
releases_bucket='s3://releases.staging.lix.systems',
|
||||||
git_repo=lambda: 'ssh://git@git.lix.systems/lix-project/lix-releng-staging',
|
git_repo=lambda: 'ssh://git@git.lix.systems/lix-project/lix-releng-staging',
|
||||||
git_repo_is_gerrit=False,
|
git_repo_is_gerrit=False,
|
||||||
docker_targets=[
|
docker_targets=[
|
||||||
@@ -106,8 +111,10 @@ STAGING = RelengEnvironment(
|
|||||||
DockerTarget('ghcr.io/lix-project/lix-releng-staging',
|
DockerTarget('ghcr.io/lix-project/lix-releng-staging',
|
||||||
tags=['{version}', '{major}']),
|
tags=['{version}', '{major}']),
|
||||||
],
|
],
|
||||||
s3_endpoint = 'https://s3.lix.systems',
|
s3_endpoint = 'https://s3.afnix.fr',
|
||||||
|
s3_region = 'garage',
|
||||||
s3_ssh_host = S3_HOST,
|
s3_ssh_host = S3_HOST,
|
||||||
|
s3_ssh_user = S3_USER,
|
||||||
)
|
)
|
||||||
|
|
||||||
GERRIT_REMOTE_RE = re.compile(r'^ssh://(\w+@)?gerrit.lix.systems:2022/lix$')
|
GERRIT_REMOTE_RE = re.compile(r'^ssh://(\w+@)?gerrit.lix.systems:2022/lix$')
|
||||||
@@ -127,13 +134,13 @@ def guess_gerrit_remote():
|
|||||||
PROD = RelengEnvironment(
|
PROD = RelengEnvironment(
|
||||||
name='production',
|
name='production',
|
||||||
colour=functools.partial(sgr, RED),
|
colour=functools.partial(sgr, RED),
|
||||||
docs_bucket='s3://docs',
|
docs_bucket='s3://docs.lix.systems',
|
||||||
cache_bucket='s3://cache',
|
cache_bucket='s3://cache.lix.systems',
|
||||||
# FIXME: we should decrypt this with age into a tempdir in the future, but
|
# FIXME: we should decrypt this with age into a tempdir in the future, but
|
||||||
# the issue is how to deal with the recipients file. For now, we should
|
# the issue is how to deal with the recipients file. For now, we should
|
||||||
# just delete it after doing a release.
|
# just delete it after doing a release.
|
||||||
cache_store_overlay={'secret-key': 'prod.key'},
|
cache_store_overlay={'secret-key': 'prod.key'},
|
||||||
releases_bucket='s3://releases',
|
releases_bucket='s3://releases.lix.systems',
|
||||||
git_repo=guess_gerrit_remote,
|
git_repo=guess_gerrit_remote,
|
||||||
git_repo_is_gerrit=True,
|
git_repo_is_gerrit=True,
|
||||||
docker_targets=[
|
docker_targets=[
|
||||||
@@ -142,8 +149,10 @@ PROD = RelengEnvironment(
|
|||||||
tags=['{version}', '{major}']),
|
tags=['{version}', '{major}']),
|
||||||
DockerTarget('ghcr.io/lix-project/lix', tags=['{version}', '{major}']),
|
DockerTarget('ghcr.io/lix-project/lix', tags=['{version}', '{major}']),
|
||||||
],
|
],
|
||||||
s3_endpoint = 'https://s3.lix.systems',
|
s3_endpoint = 'https://s3.afnix.fr',
|
||||||
|
s3_region = 'global',
|
||||||
s3_ssh_host = S3_HOST,
|
s3_ssh_host = S3_HOST,
|
||||||
|
s3_ssh_user = S3_USER,
|
||||||
)
|
)
|
||||||
|
|
||||||
ENVIRONMENTS = {
|
ENVIRONMENTS = {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# SPDX-FileCopyrightText: 2024 Jade Lovelace
|
# SPDX-FileCopyrightText: 2024 Jade Lovelace
|
||||||
|
# SPDX-FileCopyrightText: 2026 Yureka Lilian <yureka@cyberchaos.dev>
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import datetime
|
import datetime
|
||||||
import dataclasses
|
|
||||||
import re
|
import re
|
||||||
from typing import Any, Literal, Optional
|
from typing import Any
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
@@ -14,27 +14,34 @@ import logging
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
fmt = logging.Formatter('{asctime} {levelname} {name}: {message}',
|
fmt = logging.Formatter(
|
||||||
datefmt='%b %d %H:%M:%S',
|
"{asctime} {levelname} {name}: {message}",
|
||||||
style='{')
|
datefmt="%b %d %H:%M:%S",
|
||||||
|
style="{",
|
||||||
|
)
|
||||||
|
|
||||||
if not any(isinstance(h, logging.StreamHandler) for h in log.handlers):
|
if not any(isinstance(h, logging.StreamHandler) for h in log.handlers):
|
||||||
hand = logging.StreamHandler()
|
hand = logging.StreamHandler()
|
||||||
hand.setFormatter(fmt)
|
hand.setFormatter(fmt)
|
||||||
log.addHandler(hand)
|
log.addHandler(hand)
|
||||||
|
|
||||||
API_BASE = os.environ.get('GARAGE_ADMIN_API_BASE', 'http://localhost:3903')
|
API_BASE = os.environ.get("GARAGE_ADMIN_API_BASE", "http://localhost:3903")
|
||||||
API_KEY = os.environ['GARAGE_ADMIN_TOKEN']
|
API_KEY = os.environ["GARAGE_ADMIN_TOKEN"]
|
||||||
|
|
||||||
|
BUCKET_REGEX_STR = os.environ.get("BUCKET_REGEX", ".*")
|
||||||
|
BUCKET_REGEX = re.compile(BUCKET_REGEX_STR)
|
||||||
|
|
||||||
|
|
||||||
def api(method, endpoint: str, resp_json=True, **kwargs) -> Any:
|
def api(method, endpoint: str, resp_json=True, **kwargs) -> Any:
|
||||||
log.info('http %s %s', method, endpoint)
|
log.info("http %s %s", method, endpoint)
|
||||||
if not endpoint.startswith('https'):
|
if not endpoint.startswith("https"):
|
||||||
endpoint = API_BASE + endpoint
|
endpoint = API_BASE + endpoint
|
||||||
resp = requests.request(method,
|
resp = requests.request(
|
||||||
|
method,
|
||||||
endpoint,
|
endpoint,
|
||||||
headers={'Authorization': f'Bearer {API_KEY}'},
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
**kwargs)
|
**kwargs,
|
||||||
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
if resp_json:
|
if resp_json:
|
||||||
return resp.json()
|
return resp.json()
|
||||||
@@ -42,97 +49,64 @@ def api(method, endpoint: str, resp_json=True, **kwargs) -> Any:
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
def get_bucket_id(bucket_name: str) -> str:
|
||||||
class Key:
|
resp: dict = api(
|
||||||
name: str
|
"GET", "/v2/GetBucketInfo", params={"globalAlias": bucket_name}
|
||||||
id: str
|
)
|
||||||
secret_key: Optional[str] = None
|
return resp["id"]
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
DATEFMT = "%Y%m%d%H%M%S"
|
||||||
class Bucket:
|
|
||||||
id: str
|
|
||||||
|
|
||||||
|
|
||||||
def keys() -> list[Key]:
|
|
||||||
data: list[dict] = api('GET', '/v1/key?list')
|
|
||||||
return [Key(name=k['name'], id=k['id']) for k in data]
|
|
||||||
|
|
||||||
|
|
||||||
def delete_key(key: Key):
|
|
||||||
api('DELETE', '/v1/key', resp_json=False, params={'id': key.id})
|
|
||||||
|
|
||||||
|
|
||||||
def create_key(name: str) -> Key:
|
|
||||||
resp: dict = api('POST', '/v1/key', json={'name': name})
|
|
||||||
return Key(name=resp['name'],
|
|
||||||
id=resp['accessKeyId'],
|
|
||||||
secret_key=resp['secretAccessKey'])
|
|
||||||
|
|
||||||
|
|
||||||
AccessType = Literal['read'] | Literal['write'] | Literal['owner']
|
|
||||||
|
|
||||||
|
|
||||||
def get_bucket(bucket_name: str) -> Bucket:
|
|
||||||
resp: dict = api('GET', '/v1/bucket', params={'globalAlias': bucket_name})
|
|
||||||
return Bucket(resp['id'])
|
|
||||||
|
|
||||||
|
|
||||||
def grant(bucket: Bucket, access_types: list[AccessType], key: Key):
|
|
||||||
access_types_dict = {k: True for k in access_types}
|
|
||||||
api('POST',
|
|
||||||
'/v1/bucket/allow',
|
|
||||||
json={
|
|
||||||
'bucketId': bucket.id,
|
|
||||||
'accessKeyId': key.id,
|
|
||||||
'permissions': access_types_dict,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
KEY_RE = re.compile(r'^.*ephemeral-(\d{14})$')
|
|
||||||
DATEFMT = '%Y%m%d%H%M%S'
|
|
||||||
|
|
||||||
|
|
||||||
def expired_keys(older_than: datetime.datetime) -> list[Key]:
|
|
||||||
ret = []
|
|
||||||
for key in keys():
|
|
||||||
if m := KEY_RE.match(key.name):
|
|
||||||
date = datetime.datetime.strptime(m.group(1), DATEFMT)
|
|
||||||
date = date.astimezone(datetime.UTC)
|
|
||||||
print(date)
|
|
||||||
if date < older_than:
|
|
||||||
ret.append(key)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def do_new(args):
|
def do_new(args):
|
||||||
buckets = [get_bucket(b) for b in args.buckets]
|
for b in args.buckets:
|
||||||
|
if not BUCKET_REGEX.match(b):
|
||||||
|
print(f"Bucket {b} not in allowed buckeds '{BUCKET_REGEX_STR}'")
|
||||||
|
exit(1)
|
||||||
|
bucket_ids = [get_bucket_id(b) for b in args.buckets]
|
||||||
|
|
||||||
def optional(s: str, whether) -> list[str]:
|
key_name = args.name + "-" if args.name else ""
|
||||||
if whether:
|
expiration = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(
|
||||||
return [s]
|
seconds=args.age_secs
|
||||||
else:
|
)
|
||||||
return []
|
key_name += "ephemeral-" + expiration.strftime(DATEFMT)
|
||||||
|
|
||||||
access_types: list[AccessType] = optional('read', args.read) + optional(
|
key_resp: dict = api(
|
||||||
'write', args.write) + optional('owner', args.owner) # type: ignore
|
"POST",
|
||||||
|
"/v2/CreateKey",
|
||||||
|
json={
|
||||||
|
"name": key_name,
|
||||||
|
"expiration": expiration.isoformat(),
|
||||||
|
"neverExpires": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
key_name = args.name + '-' if args.name else ''
|
for b in bucket_ids:
|
||||||
key_name += "ephemeral-" + (
|
api(
|
||||||
datetime.datetime.now(tz=datetime.UTC) +
|
"POST",
|
||||||
datetime.timedelta(seconds=args.age_secs)).strftime(DATEFMT)
|
"/v2/AllowBucketKey",
|
||||||
|
json={
|
||||||
|
"accessKeyId": key_resp["accessKeyId"],
|
||||||
|
"bucketId": b,
|
||||||
|
"permissions": {
|
||||||
|
"read": args.read,
|
||||||
|
"write": args.write,
|
||||||
|
"owner": args.owner,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
k = create_key(key_name)
|
print(
|
||||||
for b in buckets:
|
json.dumps(
|
||||||
grant(b, access_types, k)
|
{
|
||||||
|
"name": key_resp["name"],
|
||||||
print(json.dumps(dataclasses.asdict(k), indent=2))
|
"id": key_resp["accessKeyId"],
|
||||||
|
"secret_key": key_resp["secretAccessKey"],
|
||||||
|
},
|
||||||
def do_clean(args):
|
indent=2,
|
||||||
older_than = datetime.datetime.now(tz=datetime.UTC)
|
)
|
||||||
for key in expired_keys(older_than):
|
)
|
||||||
delete_key(key)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -148,28 +122,27 @@ def main():
|
|||||||
|
|
||||||
new = sps.add_parser("new", help="Make an ephemeral key")
|
new = sps.add_parser("new", help="Make an ephemeral key")
|
||||||
new.add_argument("--name", help="Name prefix for the key")
|
new.add_argument("--name", help="Name prefix for the key")
|
||||||
new.add_argument("--read",
|
new.add_argument(
|
||||||
action="store_true",
|
"--read", action="store_true", help="Grant read access to buckets"
|
||||||
help="Grant read access to buckets")
|
)
|
||||||
new.add_argument("--write",
|
new.add_argument(
|
||||||
action="store_true",
|
"--write", action="store_true", help="Grant write access to buckets"
|
||||||
help="Grant write access to buckets")
|
)
|
||||||
new.add_argument("--owner",
|
new.add_argument(
|
||||||
action="store_true",
|
"--owner", action="store_true", help="Grant owner access to buckets"
|
||||||
help="Grant owner access to buckets")
|
)
|
||||||
new.add_argument("--age-secs",
|
new.add_argument(
|
||||||
|
"--age-secs",
|
||||||
type=int,
|
type=int,
|
||||||
required=True,
|
required=True,
|
||||||
help="Maximum key lifetime in seconds")
|
help="Maximum key lifetime in seconds",
|
||||||
new.add_argument("buckets", nargs='*', help="Buckets to grant access to")
|
)
|
||||||
|
new.add_argument("buckets", nargs="*", help="Buckets to grant access to")
|
||||||
new.set_defaults(cmd=do_new)
|
new.set_defaults(cmd=do_new)
|
||||||
|
|
||||||
clean = sps.add_parser("clean", help="Clean up old keys")
|
|
||||||
clean.set_defaults(cmd=do_clean)
|
|
||||||
|
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
args.cmd(args)
|
args.cmd(args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ def get_ephemeral_key(
|
|||||||
env.docs_bucket.removeprefix('s3://'),
|
env.docs_bucket.removeprefix('s3://'),
|
||||||
]
|
]
|
||||||
if env.s3_ssh_host is not None:
|
if env.s3_ssh_host is not None:
|
||||||
command = ['ssh', '-l', 'root', env.s3_ssh_host, *command]
|
command = ['ssh', f'{env.s3_ssh_user}@{env.s3_ssh_host}', *command]
|
||||||
output = subprocess.check_output(command)
|
output = subprocess.check_output(command)
|
||||||
d = json.loads(output.decode())
|
d = json.loads(output.decode())
|
||||||
return environment.S3Credentials(name=d['name'],
|
return environment.S3Credentials(name=d['name'],
|
||||||
|
|||||||
@@ -15,6 +15,18 @@ rec {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cycle-with-deps = mkDerivation {
|
||||||
|
name = "cycle-with-deps";
|
||||||
|
inherit dep;
|
||||||
|
outputs = [ "foo" "bar" ];
|
||||||
|
builder = builtins.toFile "builder.sh" ''
|
||||||
|
mkdir -p $foo/bin $bar/lib
|
||||||
|
ln -sf $dep $bar/lib
|
||||||
|
echo $foo > $bar/txt
|
||||||
|
echo $bar > $foo/txt
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
as_dependency = mkDerivation {
|
as_dependency = mkDerivation {
|
||||||
name = "depends-on-cycle";
|
name = "depends-on-cycle";
|
||||||
inherit cycle;
|
inherit cycle;
|
||||||
|
|||||||
@@ -16,3 +16,14 @@ error="$(! nix-build check-outputs.nix -A as_dependency 2>&1)"
|
|||||||
|
|
||||||
grepQuiet "cycle detected in build of '.*' in the references of output 'bar' from output 'foo'" <<<"$error"
|
grepQuiet "cycle detected in build of '.*' in the references of output 'bar' from output 'foo'" <<<"$error"
|
||||||
grepQuiet "error: 1 dependencies of derivation" <<<"$error"
|
grepQuiet "error: 1 dependencies of derivation" <<<"$error"
|
||||||
|
|
||||||
|
error="$(! nix-build check-outputs.nix -A cycle-with-deps 2>&1)"
|
||||||
|
grepQuiet "cycle detected in build of '.*' in the references of output 'bar' from output 'foo'" <<<"$error"
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" = Linux ]]; then
|
||||||
|
echo "$error"
|
||||||
|
<<<"$error" grepQuiet "/store/.*-cycle-with-deps-bar"
|
||||||
|
<<<"$error" grepQuiet "└───txt: ….*cycle-with-deps-foo.*"
|
||||||
|
<<<"$error" grepQuiet " →.*/store/.*-cycle-with-deps-foo"
|
||||||
|
<<<"$error" grepQuiet " └───txt:.*-cycle-with-deps-bar.*"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"234"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
let
|
||||||
|
a = builtins.substring 1;
|
||||||
|
b = a 3;
|
||||||
|
in
|
||||||
|
b "1234567890"
|
||||||
@@ -521,4 +521,27 @@ INSTANTIATE_TEST_SUITE_P(
|
|||||||
concat({header, make_directory({{"DE", make_file(false, "meow")}, {"de", make_file(false, "mrrp")}})})
|
concat({header, make_directory({{"DE", make_file(false, "meow")}, {"de", make_file(false, "mrrp")}})})
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TEST_F(NarTest, stringSizeLimit)
|
||||||
|
{
|
||||||
|
GeneratorSource source([]() -> Generator<Bytes> {
|
||||||
|
const char preamble[] =
|
||||||
|
"\x0d\x00\x00\x00\x00\x00\x00\x00nix-archive-1\x00\x00\x00"
|
||||||
|
"\x01\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00"
|
||||||
|
"\x04\x00\x00\x00\x00\x00\x00\x00type\x00\x00\x00\x00";
|
||||||
|
co_yield Bytes{preamble, sizeof(preamble) - 1};
|
||||||
|
// the nar parser keeps all strings in a buffer with the 8 byte length prefix in front.
|
||||||
|
// sufficiently large strings overflowed caused the buffer size calculation to overflow
|
||||||
|
// and thus allowed out-of-bounds writes in the daemon and potentially privesc to root.
|
||||||
|
co_yield Bytes{"\xf7\xff\xff\xff\xff\xff\xff\xff", 8};
|
||||||
|
// overflow would happen while reading data
|
||||||
|
while (true) {
|
||||||
|
co_yield Bytes{"foo-", 4};
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
|
||||||
|
auto parser = nar::parse(source);
|
||||||
|
|
||||||
|
ASSERT_THROW(parser.next(), SerialisationError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.94.0",
|
"version": "2.94.2",
|
||||||
"official_release": false,
|
"official_release": true,
|
||||||
"release_name": "Açaí na tigela"
|
"release_name": "Açaí na tigela"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user