Compare commits
20
Commits
main
...
release-2.94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ac0d9c19 | ||
|
|
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
|
||||
github: detroyejr
|
||||
|
||||
edef:
|
||||
github: edef1c
|
||||
|
||||
edolstra:
|
||||
display_name: Eelco Dolstra
|
||||
github: edolstra
|
||||
@@ -229,6 +232,9 @@ roberth:
|
||||
display_name: Robert Hensing
|
||||
github: roberth
|
||||
|
||||
sandydoo:
|
||||
github: sandydoo
|
||||
|
||||
seppel3210:
|
||||
github: Seppel3210
|
||||
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
# 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)
|
||||
|
||||
@@ -74,7 +74,11 @@ std::string renderMarkdownToTerminal(std::string_view markdown, StandardOutputSt
|
||||
.vmargin = 0,
|
||||
#endif /* LOWDOWN_SEPARATE_TERM_OPTS */
|
||||
.feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES,
|
||||
#ifdef LOWDOWN_CONSOLIDATED_OFLAGS
|
||||
.oflags = LOWDOWN_NOLINK,
|
||||
#else
|
||||
.oflags = LOWDOWN_TERM_NOLINK,
|
||||
#endif /* LOWDOWN_CONSOLIDATED_OFLAGS */
|
||||
};
|
||||
if (!shouldANSI(fileno)) {
|
||||
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)
|
||||
: 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->_n = args.size();
|
||||
std::copy(args.begin(), args.end(), app->_args);
|
||||
app->_n = baseArgs.size() + moreArgs.size();
|
||||
std::copy(baseArgs.begin(), baseArgs.end(), app->_args);
|
||||
std::copy(moreArgs.begin(), moreArgs.end(), app->_args + baseArgs.size());
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -544,6 +544,10 @@ public:
|
||||
/// lazy and/or partial application of a function.
|
||||
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
|
||||
/// by plugins. Do any existing plugins even use this mechanism?
|
||||
Value(external_t, ExternalValueBase & external)
|
||||
|
||||
@@ -225,6 +225,7 @@ try {
|
||||
|
||||
auto * buildIdDir = std::get_if<nar_index::Directory>(&narIndex);
|
||||
for (auto subdir : { "lib", "debug", ".build-id" }) {
|
||||
if (!buildIdDir) break;
|
||||
// get returns nullptr subdir does not exist, and std::get_if propagates it.
|
||||
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_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()));
|
||||
result.storePath = drvPath;
|
||||
|
||||
@@ -246,7 +246,7 @@ struct DerivationGoal : public Goal
|
||||
|
||||
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.
|
||||
@@ -307,6 +307,8 @@ struct DerivationGoal : public Goal
|
||||
virtual void cleanupPostOutputsRegisteredModeNonCheck();
|
||||
|
||||
protected:
|
||||
AsyncSemaphore::Token slotToken;
|
||||
|
||||
kj::TimePoint lastChildActivity = kj::minValue;
|
||||
|
||||
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
|
||||
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());
|
||||
|
||||
trace("done");
|
||||
|
||||
@@ -82,9 +82,6 @@ struct Goal
|
||||
*/
|
||||
std::string name;
|
||||
|
||||
protected:
|
||||
AsyncSemaphore::Token slotToken;
|
||||
|
||||
public:
|
||||
struct [[nodiscard]] WorkResult {
|
||||
ExitCode exitCode;
|
||||
|
||||
@@ -1825,15 +1825,18 @@ try {
|
||||
);
|
||||
}
|
||||
|
||||
outputGraph[scratchOutputs.at(name)] = StorePathSet{};
|
||||
std::visit(
|
||||
overloaded{/* Since we'll use the already installed versions of these, we
|
||||
can treat them as leaves and ignore any references they
|
||||
have. */
|
||||
[&](const AlreadyRegistered &) {
|
||||
outputGraph[scratchOutputs.at(name)] = StorePathSet{};
|
||||
},
|
||||
[&](const AlreadyRegistered &) {},
|
||||
[&](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
|
||||
@@ -1844,10 +1847,8 @@ try {
|
||||
topoSort(outputsToSort, {[&](const std::string & name) {
|
||||
StringSet dependencies;
|
||||
for (auto & path : outputGraph.at(scratchOutputs.at(name))) {
|
||||
auto outputName = inverseOutputMap.find(path);
|
||||
if (outputName != inverseOutputMap.end()) {
|
||||
dependencies.insert(outputName->second);
|
||||
}
|
||||
auto outputName = inverseOutputMap.at(path);
|
||||
dependencies.insert(outputName);
|
||||
}
|
||||
return dependencies;
|
||||
}});
|
||||
|
||||
@@ -27,13 +27,6 @@ PathSubstitutionGoal::PathSubstitutionGoal(
|
||||
maintainExpectedSubstitutions = worker.expectedSubstitutions.addTemporarily(1);
|
||||
}
|
||||
|
||||
|
||||
PathSubstitutionGoal::~PathSubstitutionGoal()
|
||||
{
|
||||
cleanup();
|
||||
}
|
||||
|
||||
|
||||
Goal::WorkResult PathSubstitutionGoal::done(
|
||||
ExitCode result,
|
||||
BuildResult::Status status,
|
||||
@@ -76,8 +69,6 @@ kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryNext() noexcept
|
||||
try {
|
||||
trace("trying next substituter");
|
||||
|
||||
cleanup();
|
||||
|
||||
if (subs.size() == 0) {
|
||||
/* None left. Terminate this goal and let someone else deal
|
||||
with it. */
|
||||
@@ -205,29 +196,20 @@ kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryToRun() noexcept
|
||||
try {
|
||||
trace("trying to run");
|
||||
|
||||
if (!slotToken.valid()) {
|
||||
slotToken = co_await worker.substitutions.acquire();
|
||||
}
|
||||
auto & fetchPath = subPath ? *subPath : storePath;
|
||||
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);
|
||||
|
||||
auto pipe = kj::newPromiseAndCrossThreadFulfiller<void>();
|
||||
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(
|
||||
TRY_AWAIT(copyStorePath(
|
||||
*sub,
|
||||
worker.store,
|
||||
fetchPath,
|
||||
@@ -235,6 +217,8 @@ try {
|
||||
sub->config().isTrusted ? NoCheckSigs : CheckSigs,
|
||||
&act
|
||||
));
|
||||
|
||||
break;
|
||||
} catch (const EndOfFile &) {
|
||||
throw EndOfFile(
|
||||
"NAR for '%s' fetched from '%s' is incomplete",
|
||||
@@ -242,24 +226,6 @@ try {
|
||||
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)
|
||||
printError("%1%", Uncolored(e.what()));
|
||||
|
||||
@@ -271,10 +237,20 @@ try {
|
||||
substituterFailed = true;
|
||||
}
|
||||
}
|
||||
/* Try the next substitute. */
|
||||
|
||||
/* Try the next substitute */
|
||||
co_return co_await tryNext();
|
||||
} 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);
|
||||
|
||||
printMsg(lvlChatty, "substitution of path '%s' succeeded", worker.store.printStorePath(storePath));
|
||||
@@ -294,19 +270,4 @@ try {
|
||||
} catch (...) {
|
||||
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;
|
||||
|
||||
/**
|
||||
* The substituter thread.
|
||||
*/
|
||||
std::future<void> thr;
|
||||
|
||||
/**
|
||||
* Whether to try to repair a valid path.
|
||||
*/
|
||||
@@ -85,7 +80,6 @@ public:
|
||||
RepairFlag repair = NoRepair,
|
||||
std::optional<ContentAddress> ca = std::nullopt
|
||||
);
|
||||
~PathSubstitutionGoal();
|
||||
|
||||
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
|
||||
|
||||
@@ -97,9 +91,6 @@ public:
|
||||
kj::Promise<Result<WorkResult>> tryToRun() noexcept;
|
||||
kj::Promise<Result<WorkResult>> finished() noexcept;
|
||||
|
||||
/* Called by destructor, can't be overridden */
|
||||
void cleanup() override final;
|
||||
|
||||
JobCategory jobCategory() const override {
|
||||
return JobCategory::Substitution;
|
||||
};
|
||||
|
||||
@@ -471,6 +471,6 @@ configure_file(
|
||||
'libdir' : libdir,
|
||||
'includedir' : includedir,
|
||||
'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 & 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(); \
|
||||
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) \
|
||||
do { \
|
||||
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(
|
||||
configuration : {
|
||||
'PACKAGE_NAME': '"' + meson.project_name() + '"',
|
||||
'PACKAGE_VERSION': '"' + meson.project_version() + '"',
|
||||
'LIX_MAJOR': lix_major,
|
||||
'LIX_MINOR': lix_minor,
|
||||
'LIX_PATCH': lix_patch,
|
||||
'PACKAGE_TARNAME': '"' + meson.project_name() + '"',
|
||||
'PACKAGE_STRING': '"' + meson.project_name() + ' ' + meson.project_version() + '"',
|
||||
'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
|
||||
*/
|
||||
|
||||
@@ -350,6 +350,11 @@ if lowdown.version().version_compare('>= 1.4.0')
|
||||
add_project_arguments('-DLOWDOWN_SEPARATE_TERM_OPTS', language: 'cpp')
|
||||
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
|
||||
# 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.
|
||||
|
||||
@@ -37,7 +37,7 @@ def setup_creds(env: RelengEnvironment):
|
||||
key = keys.get_ephemeral_key(env)
|
||||
$AWS_SECRET_ACCESS_KEY = key.secret_key
|
||||
$AWS_ACCESS_KEY_ID = key.id
|
||||
$AWS_DEFAULT_REGION = 'garage'
|
||||
$AWS_DEFAULT_REGION = env.s3_region
|
||||
$AWS_ENDPOINT_URL = env.s3_endpoint
|
||||
|
||||
|
||||
|
||||
+20
-11
@@ -5,11 +5,12 @@ import functools
|
||||
import subprocess
|
||||
import dataclasses
|
||||
|
||||
S3_HOST = 's3.lix.systems'
|
||||
S3_HOST = 's3-admin.afnix.fr'
|
||||
S3_USER = 'lix-releng'
|
||||
|
||||
DEFAULT_STORE_URI_BITS = {
|
||||
'region': 'garage',
|
||||
'endpoint': 's3.lix.systems',
|
||||
'region': 'global',
|
||||
'endpoint': 's3.afnix.fr',
|
||||
'want-mass-query': 'true',
|
||||
'write-nar-listing': 'true',
|
||||
'ls-compression': 'zstd',
|
||||
@@ -54,7 +55,9 @@ class RelengEnvironment:
|
||||
git_repo: Callable[[], str]
|
||||
git_repo_is_gerrit: bool
|
||||
s3_endpoint: str
|
||||
s3_region: str
|
||||
s3_ssh_host: str | None
|
||||
s3_ssh_user: str | None
|
||||
|
||||
docker_targets: list[DockerTarget]
|
||||
|
||||
@@ -86,17 +89,19 @@ LOCAL = RelengEnvironment(
|
||||
git_repo_is_gerrit=False,
|
||||
docker_targets=[],
|
||||
s3_endpoint = 'http://localhost:3900',
|
||||
s3_region = 'garage',
|
||||
s3_ssh_host = None,
|
||||
s3_ssh_user = None,
|
||||
)
|
||||
|
||||
|
||||
STAGING = RelengEnvironment(
|
||||
name='staging',
|
||||
colour=functools.partial(sgr, GREEN),
|
||||
docs_bucket='s3://staging-docs',
|
||||
cache_bucket='s3://staging-cache',
|
||||
docs_bucket='s3://docs.staging.lix.systems',
|
||||
cache_bucket='s3://cache.staging.lix.systems',
|
||||
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_is_gerrit=False,
|
||||
docker_targets=[
|
||||
@@ -106,8 +111,10 @@ STAGING = RelengEnvironment(
|
||||
DockerTarget('ghcr.io/lix-project/lix-releng-staging',
|
||||
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_user = S3_USER,
|
||||
)
|
||||
|
||||
GERRIT_REMOTE_RE = re.compile(r'^ssh://(\w+@)?gerrit.lix.systems:2022/lix$')
|
||||
@@ -127,13 +134,13 @@ def guess_gerrit_remote():
|
||||
PROD = RelengEnvironment(
|
||||
name='production',
|
||||
colour=functools.partial(sgr, RED),
|
||||
docs_bucket='s3://docs',
|
||||
cache_bucket='s3://cache',
|
||||
docs_bucket='s3://docs.lix.systems',
|
||||
cache_bucket='s3://cache.lix.systems',
|
||||
# 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
|
||||
# just delete it after doing a release.
|
||||
cache_store_overlay={'secret-key': 'prod.key'},
|
||||
releases_bucket='s3://releases',
|
||||
releases_bucket='s3://releases.lix.systems',
|
||||
git_repo=guess_gerrit_remote,
|
||||
git_repo_is_gerrit=True,
|
||||
docker_targets=[
|
||||
@@ -142,8 +149,10 @@ PROD = RelengEnvironment(
|
||||
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_user = S3_USER,
|
||||
)
|
||||
|
||||
ENVIRONMENTS = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# SPDX-FileCopyrightText: 2024 Jade Lovelace
|
||||
# SPDX-FileCopyrightText: 2026 Yureka Lilian <yureka@cyberchaos.dev>
|
||||
# SPDX-License-Identifier: MIT
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import datetime
|
||||
import dataclasses
|
||||
import re
|
||||
from typing import Any, Literal, Optional
|
||||
from typing import Any
|
||||
import requests
|
||||
import os
|
||||
import logging
|
||||
@@ -14,27 +14,34 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
fmt = logging.Formatter('{asctime} {levelname} {name}: {message}',
|
||||
datefmt='%b %d %H:%M:%S',
|
||||
style='{')
|
||||
fmt = logging.Formatter(
|
||||
"{asctime} {levelname} {name}: {message}",
|
||||
datefmt="%b %d %H:%M:%S",
|
||||
style="{",
|
||||
)
|
||||
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in log.handlers):
|
||||
hand = logging.StreamHandler()
|
||||
hand.setFormatter(fmt)
|
||||
log.addHandler(hand)
|
||||
|
||||
API_BASE = os.environ.get('GARAGE_ADMIN_API_BASE', 'http://localhost:3903')
|
||||
API_KEY = os.environ['GARAGE_ADMIN_TOKEN']
|
||||
API_BASE = os.environ.get("GARAGE_ADMIN_API_BASE", "http://localhost:3903")
|
||||
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:
|
||||
log.info('http %s %s', method, endpoint)
|
||||
if not endpoint.startswith('https'):
|
||||
log.info("http %s %s", method, endpoint)
|
||||
if not endpoint.startswith("https"):
|
||||
endpoint = API_BASE + endpoint
|
||||
resp = requests.request(method,
|
||||
resp = requests.request(
|
||||
method,
|
||||
endpoint,
|
||||
headers={'Authorization': f'Bearer {API_KEY}'},
|
||||
**kwargs)
|
||||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||
**kwargs,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
if resp_json:
|
||||
return resp.json()
|
||||
@@ -42,97 +49,64 @@ def api(method, endpoint: str, resp_json=True, **kwargs) -> Any:
|
||||
return resp
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Key:
|
||||
name: str
|
||||
id: str
|
||||
secret_key: Optional[str] = None
|
||||
def get_bucket_id(bucket_name: str) -> str:
|
||||
resp: dict = api(
|
||||
"GET", "/v2/GetBucketInfo", params={"globalAlias": bucket_name}
|
||||
)
|
||||
return resp["id"]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
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
|
||||
DATEFMT = "%Y%m%d%H%M%S"
|
||||
|
||||
|
||||
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]:
|
||||
if whether:
|
||||
return [s]
|
||||
else:
|
||||
return []
|
||||
key_name = args.name + "-" if args.name else ""
|
||||
expiration = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(
|
||||
seconds=args.age_secs
|
||||
)
|
||||
key_name += "ephemeral-" + expiration.strftime(DATEFMT)
|
||||
|
||||
access_types: list[AccessType] = optional('read', args.read) + optional(
|
||||
'write', args.write) + optional('owner', args.owner) # type: ignore
|
||||
key_resp: dict = api(
|
||||
"POST",
|
||||
"/v2/CreateKey",
|
||||
json={
|
||||
"name": key_name,
|
||||
"expiration": expiration.isoformat(),
|
||||
"neverExpires": False,
|
||||
},
|
||||
)
|
||||
|
||||
key_name = args.name + '-' if args.name else ''
|
||||
key_name += "ephemeral-" + (
|
||||
datetime.datetime.now(tz=datetime.UTC) +
|
||||
datetime.timedelta(seconds=args.age_secs)).strftime(DATEFMT)
|
||||
for b in bucket_ids:
|
||||
api(
|
||||
"POST",
|
||||
"/v2/AllowBucketKey",
|
||||
json={
|
||||
"accessKeyId": key_resp["accessKeyId"],
|
||||
"bucketId": b,
|
||||
"permissions": {
|
||||
"read": args.read,
|
||||
"write": args.write,
|
||||
"owner": args.owner,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
k = create_key(key_name)
|
||||
for b in buckets:
|
||||
grant(b, access_types, k)
|
||||
|
||||
print(json.dumps(dataclasses.asdict(k), indent=2))
|
||||
|
||||
|
||||
def do_clean(args):
|
||||
older_than = datetime.datetime.now(tz=datetime.UTC)
|
||||
for key in expired_keys(older_than):
|
||||
delete_key(key)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"name": key_resp["name"],
|
||||
"id": key_resp["accessKeyId"],
|
||||
"secret_key": key_resp["secretAccessKey"],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -148,28 +122,27 @@ def main():
|
||||
|
||||
new = sps.add_parser("new", help="Make an ephemeral key")
|
||||
new.add_argument("--name", help="Name prefix for the key")
|
||||
new.add_argument("--read",
|
||||
action="store_true",
|
||||
help="Grant read access to buckets")
|
||||
new.add_argument("--write",
|
||||
action="store_true",
|
||||
help="Grant write access to buckets")
|
||||
new.add_argument("--owner",
|
||||
action="store_true",
|
||||
help="Grant owner access to buckets")
|
||||
new.add_argument("--age-secs",
|
||||
new.add_argument(
|
||||
"--read", action="store_true", help="Grant read access to buckets"
|
||||
)
|
||||
new.add_argument(
|
||||
"--write", action="store_true", help="Grant write access to buckets"
|
||||
)
|
||||
new.add_argument(
|
||||
"--owner", action="store_true", help="Grant owner access to buckets"
|
||||
)
|
||||
new.add_argument(
|
||||
"--age-secs",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Maximum key lifetime in seconds")
|
||||
new.add_argument("buckets", nargs='*', help="Buckets to grant access to")
|
||||
help="Maximum key lifetime in seconds",
|
||||
)
|
||||
new.add_argument("buckets", nargs="*", help="Buckets to grant access to")
|
||||
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.cmd(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ def get_ephemeral_key(
|
||||
env.docs_bucket.removeprefix('s3://'),
|
||||
]
|
||||
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)
|
||||
d = json.loads(output.decode())
|
||||
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 {
|
||||
name = "depends-on-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 "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")}})})
|
||||
))
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.94.0",
|
||||
"version": "2.94.2",
|
||||
"official_release": false,
|
||||
"release_name": "Açaí na tigela"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user