Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
753944a172 | ||
|
|
23c66f4df0 | ||
|
|
1898a6e05a | ||
|
|
247436669d | ||
|
|
fe0953923f | ||
|
|
609bc41e6f | ||
|
|
89999119dc | ||
|
|
71043329f1 | ||
|
|
ecaf293c4f | ||
|
|
09cb1fbca9 | ||
|
|
23620d0b7a | ||
|
|
351dbdfdca | ||
|
|
128159a717 | ||
|
|
33f713fa5d | ||
|
|
f92aaa9f59 | ||
|
|
e70bbff278 | ||
|
|
d2f8263fb8 | ||
|
|
15421962e6 | ||
|
|
a10fd676f6 | ||
|
|
bc506e4f67 | ||
|
|
f156a27cf4 | ||
|
|
7166c00efc |
@@ -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
|
||||||
@@ -249,6 +252,9 @@ rootile:
|
|||||||
display_name: rootile (Rutile)
|
display_name: rootile (Rutile)
|
||||||
forgejo: rootile
|
forgejo: rootile
|
||||||
|
|
||||||
|
sandydoo:
|
||||||
|
github: sandydoo
|
||||||
|
|
||||||
seppel3210:
|
seppel3210:
|
||||||
github: Seppel3210
|
github: Seppel3210
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,65 @@
|
|||||||
# Lix 2.95 "Kakigōri" (2026-03-13)
|
# Lix 2.95 "Kakigōri" (2026-03-13)
|
||||||
|
# Lix 2.95.3 (2026-05-08)
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fix upgrade-nix breaking its own access to the daemon [lix#1189](https://git.lix.systems/lix-project/lix/issues/1189) [lix#1207](https://git.lix.systems/lix-project/lix/issues/1207) [cl/5504](https://gerrit.lix.systems/c/lix/+/5504) [cl/5567](https://gerrit.lix.systems/c/lix/+/5567)
|
||||||
|
|
||||||
|
`nix upgrade-nix`, and the helper script `misc/upgrade-lix.sh` now pass `--store local` to all Nix commands, so the upgrade process can make changes to the daemon without breaking further steps in the upgrade.
|
||||||
|
|
||||||
|
Many thanks to [Qyriad](https://git.lix.systems/Qyriad) for this.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Lix 2.95.2 (2026-05-04)
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fix unsigned overflow leading to out-of-band write in the NAR parser [cl/5550](https://gerrit.lix.systems/c/lix/+/5550)
|
||||||
|
|
||||||
|
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.95.1 (2026-03-19)
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- fix static builds [cl/5385](https://gerrit.lix.systems/c/lix/+/5385)
|
||||||
|
|
||||||
|
Static builds using musl were broken in 2.95.0 and should work again now.
|
||||||
|
|
||||||
|
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
|
||||||
|
|
||||||
|
- flake config warnings are now printed to stderr [lix#1155](https://git.lix.systems/lix-project/lix/issues/1155) [cl/5379](https://gerrit.lix.systems/c/lix/+/5379)
|
||||||
|
|
||||||
|
The settings listed in a flake-config confirmation prompt are now printed to stderr rather than stdout, which allows `nix print-dev-env` to emit valid bash again even in the presence of untrusted settings.
|
||||||
|
|
||||||
|
Many thanks to [lheckemann](https://git.lix.systems/lheckemann) for this.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Lix 2.95.0 (2026-03-13)
|
# Lix 2.95.0 (2026-03-13)
|
||||||
|
|||||||
@@ -219,6 +219,9 @@
|
|||||||
inherit versionSuffix officialRelease;
|
inherit versionSuffix officialRelease;
|
||||||
stdenv = currentStdenv;
|
stdenv = currentStdenv;
|
||||||
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;
|
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;
|
||||||
|
# See below
|
||||||
|
lowdown = final.lowdown_3_0;
|
||||||
|
lowdown-unsandboxed = final.lowdown_3_0.override { enableDarwinSandbox = false; };
|
||||||
};
|
};
|
||||||
|
|
||||||
lix-clang-tidy = final.callPackage ./subprojects/lix-clang-tidy { };
|
lix-clang-tidy = final.callPackage ./subprojects/lix-clang-tidy { };
|
||||||
@@ -245,9 +248,21 @@
|
|||||||
# And same thing for our build-release-notes package.
|
# And same thing for our build-release-notes package.
|
||||||
build-release-notes = final.nix.passthru.build-release-notes;
|
build-release-notes = final.nix.passthru.build-release-notes;
|
||||||
|
|
||||||
lowdown =
|
# As soon as Nixpkgs updates to >= 3.0.0, change to lowdown_2_0!
|
||||||
assert lib.versionAtLeast prev.lowdown.version "2.0.0";
|
# We don't change the default version in order to not change the hash
|
||||||
prev.lowdown;
|
# of Nix/Lix from upstream Nixpkgs.
|
||||||
|
lowdown_3_0 =
|
||||||
|
assert lib.versionOlder prev.lowdown.version "3.0.0";
|
||||||
|
prev.lowdown.overrideAttrs (
|
||||||
|
finalAttrs: _prevAttrs: {
|
||||||
|
version = "3.0.0";
|
||||||
|
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "https://kristaps.bsd.lv/lowdown/snapshots/lowdown-${finalAttrs.version}.tar.gz";
|
||||||
|
sha512 = "94e97234d598382c3c3dc27f9bfdb3a3a2fcf7dbb6a8df3c85ee09f27f792449034a41d49d9cfd3d8450d2de01b8562c20c3d120e65c81af4d7d6c9454119e93";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
capnproto = prev.capnproto.overrideAttrs (old: {
|
capnproto = prev.capnproto.overrideAttrs (old: {
|
||||||
patches =
|
patches =
|
||||||
@@ -275,6 +290,15 @@
|
|||||||
# Binary package for various platforms.
|
# Binary package for various platforms.
|
||||||
build = forAllSystems (system: self.packages.${system}.nix);
|
build = forAllSystems (system: self.packages.${system}.nix);
|
||||||
|
|
||||||
|
# Ensure support for lowdown < 3.0 doesn't regress for NixOS 25.11
|
||||||
|
build-lowdown_2_0.aarch64-linux = lib.genAttrs [ "aarch64-linux" ] (
|
||||||
|
system:
|
||||||
|
self.packages.${system}.nix.override {
|
||||||
|
lowdown = nixpkgsFor.${system}.native.lowdown;
|
||||||
|
lowdown-unsandboxed = nixpkgsFor.${system}.native.lowdown-unsandboxed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
# Building Lix twice in CI is expensive, but we can catch a lot of static
|
# Building Lix twice in CI is expensive, but we can catch a lot of static
|
||||||
# build regressions by at least making sure it evals and configures.
|
# build regressions by at least making sure it evals and configures.
|
||||||
configure-static = lib.genAttrs linux64BitSystems (
|
configure-static = lib.genAttrs linux64BitSystems (
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class Builtin:
|
|||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
|
||||||
{indent(self.documentation, " " * 3)}
|
{indent(self.documentation, " " * 3)}
|
||||||
|
|
||||||
{
|
{
|
||||||
f"This function is only available if the [{self.experimental_feature}](@docroot@/contributing/experimental-features.md#xp-feature-{self.experimental_feature}) experimental feature is enabled."
|
f"This function is only available if the [{self.experimental_feature}](@docroot@/contributing/experimental-features.md#xp-feature-{self.experimental_feature}) experimental feature is enabled."
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class Setting:
|
|||||||
aliases = [f"`{item}`" for item in self.aliases]
|
aliases = [f"`{item}`" for item in self.aliases]
|
||||||
description = dedent(f"""
|
description = dedent(f"""
|
||||||
|
|
||||||
{indent(indentation, self.documentation)}
|
{indent(indentation, self.documentation)}
|
||||||
|
|
||||||
{indent(indentation, PLATFORM_WARNING.format(platforms=str(platforms)[1:-1])) if self.platforms else ""}
|
{indent(indentation, PLATFORM_WARNING.format(platforms=str(platforms)[1:-1])) if self.platforms else ""}
|
||||||
{indent(indentation, XP_WARNING.format(feature=self.experimental_feature, name=self.name)) if self.experimental_feature is not None else ""}
|
{indent(indentation, XP_WARNING.format(feature=self.experimental_feature, name=self.name)) if self.experimental_feature is not None else ""}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+1
-2
@@ -867,8 +867,7 @@ void NixRepl::initBuiltinCommands()
|
|||||||
[](NixRepl & repl, const std::string & arg) {
|
[](NixRepl & repl, const std::string & arg) {
|
||||||
Value v = repl.evalString(arg);
|
Value v = repl.evalString(arg);
|
||||||
Value f = repl.evalString(
|
Value f = repl.evalString(
|
||||||
R""("drv: (import <nixpkgs> {}).runCommand "shell") ""
|
R"(drv: (import <nixpkgs> {}).runCommand "shell" { buildInputs = [ drv ]; } "")"
|
||||||
R""({ buildInputs = [ drv ]; } "")""
|
|
||||||
);
|
);
|
||||||
Value result = repl.state.callFunction(f, v, PosIdx());
|
Value result = repl.state.callFunction(f, v, PosIdx());
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "lix/libutil/rpc.hh"
|
#include "lix/libutil/rpc.hh"
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <kj/io.h>
|
#include <kj/io.h>
|
||||||
@@ -224,7 +225,11 @@ bool prepareChildSetup(build::Request::Reader request)
|
|||||||
};
|
};
|
||||||
const fs::path dst = chrootRootDir / target.relative_path();
|
const fs::path dst = chrootRootDir / target.relative_path();
|
||||||
fs::create_directories(dst.parent_path());
|
fs::create_directories(dst.parent_path());
|
||||||
writeFile(dst, std::string_view((const char *) sh, sizeof(sh)));
|
kj::AutoCloseFd fd(open(dst.c_str(), O_RDWR | O_CREAT, 0755));
|
||||||
|
if (fd == nullptr) {
|
||||||
|
throw SysError("cannot create sandbox shell");
|
||||||
|
}
|
||||||
|
writeFull(fd.get(), std::string_view((const char *) sh, sizeof(sh)));
|
||||||
fs::permissions(dst, fs::perms(0555));
|
fs::permissions(dst, fs::perms(0555));
|
||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -35,12 +35,14 @@ static bool batchAskForSetting(
|
|||||||
TrustedList & trustedList,
|
TrustedList & trustedList,
|
||||||
std::map<std::string, std::string> & untrustedSettings)
|
std::map<std::string, std::string> & untrustedSettings)
|
||||||
{
|
{
|
||||||
printWarning("The following settings require your decision:");
|
std::string warning("The following settings require your decision:");
|
||||||
for (const auto & [name, valueS] : untrustedSettings) {
|
for (const auto & [name, valueS] : untrustedSettings) {
|
||||||
// FIXME: filter ANSI escapes, newlines, \r, etc.
|
// FIXME: filter ANSI escapes, newlines, \r, etc.
|
||||||
logger->cout("- %s = %s", name, valueS);
|
warning += fmt("\n- %s = %s", name, valueS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printWarning("%s", warning);
|
||||||
|
|
||||||
auto reply = logger
|
auto reply = logger
|
||||||
->ask(
|
->ask(
|
||||||
fmt("Do you want to allow configuration settings to be applied?\nThis may allow the "
|
fmt("Do you want to allow configuration settings to be applied?\nThis may allow the "
|
||||||
|
|||||||
@@ -221,6 +221,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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1348,7 +1348,10 @@ Pid LinuxLocalDerivationGoal::startChild(AutoCloseFD setupFD, AutoCloseFD logPTY
|
|||||||
(wantUserNS ? CLONE_NEWUSER : 0) | (wantNetNS ? CLONE_NEWNET : 0) | CLONE_VM | CLONE_FILES,
|
(wantUserNS ? CLONE_NEWUSER : 0) | (wantNetNS ? CLONE_NEWNET : 0) | CLONE_VM | CLONE_FILES,
|
||||||
[]() -> int {
|
[]() -> int {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
raise(SIGSTOP);
|
// NOTE: musl apparently caches the pid of the process, which fucks with raise().
|
||||||
|
// we must explicitly use getpid() to bypass this cache instead of using raise; a
|
||||||
|
// raise(SIGSTOP) would stop the *daemon* process, and this breaks sandbox setup.
|
||||||
|
kill(getpid(), SIGSTOP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)};
|
)};
|
||||||
|
|||||||
@@ -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) { \
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ lix_doc = static_library(
|
|||||||
rnix,
|
rnix,
|
||||||
],
|
],
|
||||||
rust_args : [
|
rust_args : [
|
||||||
# Empty when default_library=static
|
|
||||||
rust_dynamic_args,
|
|
||||||
lix_doc_rust_args,
|
lix_doc_rust_args,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand
|
|||||||
// install the new one, in case the new and old versions aren't considered
|
// install the new one, in case the new and old versions aren't considered
|
||||||
// to be "the same package" by nix-env's logic (e.g., if their pnames differ).
|
// to be "the same package" by nix-env's logic (e.g., if their pnames differ).
|
||||||
Strings removeArgs = {
|
Strings removeArgs = {
|
||||||
|
"--store",
|
||||||
|
"local",
|
||||||
"--uninstall",
|
"--uninstall",
|
||||||
oldNixEnv,
|
oldNixEnv,
|
||||||
"--profile",
|
"--profile",
|
||||||
@@ -150,6 +152,8 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand
|
|||||||
aio().blockOn(runProgram(newNixEnv, false, removeArgs));
|
aio().blockOn(runProgram(newNixEnv, false, removeArgs));
|
||||||
|
|
||||||
Strings upgradeArgs = {
|
Strings upgradeArgs = {
|
||||||
|
"--store",
|
||||||
|
"local",
|
||||||
"--profile",
|
"--profile",
|
||||||
this->profileDir,
|
this->profileDir,
|
||||||
"--install",
|
"--install",
|
||||||
|
|||||||
@@ -383,6 +383,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.
|
||||||
|
|||||||
@@ -124,9 +124,12 @@ checkSystemdService
|
|||||||
|
|
||||||
set -x
|
set -x
|
||||||
exec "$NIX" run \
|
exec "$NIX" run \
|
||||||
|
--store local \
|
||||||
--extra-experimental-features "nix-command flakes" \
|
--extra-experimental-features "nix-command flakes" \
|
||||||
"${SUBSTITUTER_ARGS[@]}" \
|
"${SUBSTITUTER_ARGS[@]}" \
|
||||||
"${LIX_BASE}?ref=${LIX_REF}" \
|
"${LIX_BASE}?ref=${LIX_REF}" \
|
||||||
|
-- \
|
||||||
|
--store local \
|
||||||
--extra-experimental-features "nix-command flakes" \
|
--extra-experimental-features "nix-command flakes" \
|
||||||
upgrade-nix \
|
upgrade-nix \
|
||||||
"${SUBSTITUTER_ARGS[@]}"
|
"${SUBSTITUTER_ARGS[@]}"
|
||||||
|
|||||||
+4
-1
@@ -366,7 +366,10 @@ stdenv.mkDerivation (finalAttrs: {
|
|||||||
"-Dc_link_args=-fuse-ld=lld"
|
"-Dc_link_args=-fuse-ld=lld"
|
||||||
"-Dcpp_link_args=-fuse-ld=lld"
|
"-Dcpp_link_args=-fuse-ld=lld"
|
||||||
]
|
]
|
||||||
++ lib.optional hostPlatform.isStatic "-Denable-embedded-sandbox-shell=true"
|
++ lib.optionals hostPlatform.isStatic [
|
||||||
|
"-Denable-embedded-sandbox-shell=true"
|
||||||
|
"-Denable-contrib-plugins=false"
|
||||||
|
]
|
||||||
++ lib.optional ciBuildAndDeleteBothLibraries "-Ddefault_library=both"
|
++ lib.optional ciBuildAndDeleteBothLibraries "-Ddefault_library=both"
|
||||||
# musl doesn't support fibers, and we can't detect this with meson alone.
|
# musl doesn't support fibers, and we can't detect this with meson alone.
|
||||||
++ lib.optional hostPlatform.isMusl "-Ddisable-fibers=true"
|
++ lib.optional hostPlatform.isMusl "-Ddisable-fibers=true"
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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.95.0",
|
"version": "2.95.3",
|
||||||
"official_release": false,
|
"official_release": true,
|
||||||
"release_name": "Kakigōri"
|
"release_name": "Kakigōri"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user