Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,34 @@ name: CI

on: [push, pull_request]

permissions: {}

jobs:
lint:
runs-on: ubuntu-latest
container: debian:trixie
steps:
- name: Bootstrap Debian system package dependencies
run: |
apt-get update
apt-get install --yes --no-install-recommends \
git \
make \
python3-poetry
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install additional packages and Python dependencies
run: |
poetry install --no-ansi
- name: Run linters
run: |
make lint

build:
runs-on: ubuntu-latest
permissions:
id-token: write
strategy:
fail-fast: false
matrix:
Expand All @@ -20,6 +45,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y make build-essential
Expand Down
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ IMG_NAME = fpf.local/kernel-builder
SCRIPT_OUTPUT_PREFIX=$(PWD)/build/$(shell date +%Y%m%d)
SCRIPT_OUTPUT_EXT=log

.PHONY: lint
lint: ## Check scripts
@poetry run ruff check .
@poetry run ruff format --check .
@poetry run zizmor .

.PHONY: fix
fix: ## Fix scripts
@poetry run ruff check . --fix
@poetry run ruff format .

.PHONY: tiny-5.15
tiny-5.15: OUT:=$(SCRIPT_OUTPUT_PREFIX)-tiny-5.15.$(SCRIPT_OUTPUT_EXT)
tiny-5.15: ## Builds latest 5.15 kernel, unpatched
Expand Down
26 changes: 12 additions & 14 deletions build-kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def render_template(filename, context):
Path(filename).write_text(rendered_content)


def main():
def main(): # noqa: PLR0915
# Whether to use grsecurity patches
grsecurity = os.environ.get("GRSECURITY") == "1"
# Desired specific Linux version to download (e.g. "5.15.100")
Expand All @@ -31,7 +31,7 @@ def main():

source_date_epoch = os.environ["SOURCE_DATE_EPOCH"]
source_date_epoch_formatted = subprocess.check_output(
["date", "-R", "-d", f"@{source_date_epoch}"], text=True
["/bin/date", "-R", "-d", f"@{source_date_epoch}"], text=True
).strip()
for line in Path("/etc/os-release").read_text().splitlines():
if line.startswith("VERSION_CODENAME="):
Expand Down Expand Up @@ -63,7 +63,7 @@ def main():
print("ERROR: $LINUX_MAJOR_VERSION must be set")
sys.exit(1)
print(f"Looking up latest release of {linux_major_version} from kernel.org")
response = requests.get("https://www.kernel.org/")
response = requests.get("https://www.kernel.org/") # noqa: S113
response.raise_for_status()
linux_version = re.search(
rf"<strong>({re.escape(linux_major_version)}\.(\d+?))</strong>",
Expand All @@ -75,40 +75,38 @@ def main():
print(f"Fetching Linux kernel source {linux_version}")
subprocess.check_call(
[
"wget",
"/usr/bin/wget",
f"https://cdn.kernel.org/pub/linux/kernel/v{folder}/linux-{linux_version}.tar.xz",
]
)
subprocess.check_call(
[
"wget",
"/usr/bin/wget",
f"https://cdn.kernel.org/pub/linux/kernel/v{folder}/linux-{linux_version}.tar.sign",
]
)
print(f"Extracting Linux kernel source {linux_version}")
# We'll reuse the original tarball if we're not patching it
keep_xz = ["--keep"] if not grsecurity else []
subprocess.check_call(
["xz", "-d", "-T", "0", "-v", f"linux-{linux_version}.tar.xz"] + keep_xz
["/usr/bin/xz", "-d", "-T", "0", "-v", f"linux-{linux_version}.tar.xz"] + keep_xz
)
subprocess.check_call(
[
"gpgv",
"/usr/bin/gpgv",
"--keyring",
"/pubkeys/kroah_hartman.gpg",
f"linux-{linux_version}.tar.sign",
f"linux-{linux_version}.tar",
]
)
shutil.unpack_archive(
f"linux-{linux_version}.tar"
)
shutil.unpack_archive(f"linux-{linux_version}.tar")

# Apply grsec patches
if grsecurity:
print(f"Applying grsec patches for kernel source {linux_version}")
subprocess.check_call(
["patch", "-p", "1", "-i", "/patches-grsec/grsec"],
["/usr/bin/patch", "-p", "1", "-i", "/patches-grsec/grsec"],
cwd=f"linux-{linux_version}",
)

Expand All @@ -121,7 +119,7 @@ def main():
print("Generating orig tarball")
subprocess.check_call(
[
"tar",
"/bin/tar",
"--use-compress-program=xz -T 0",
"-cf",
orig_tarball,
Expand Down Expand Up @@ -160,14 +158,14 @@ def main():
# Building Linux kernel source
print("Building Linux kernel source", linux_version)
subprocess.check_call(
["dpkg-buildpackage", "-uc", "-us"],
["/usr/bin/dpkg-buildpackage", "-uc", "-us"],
)

os.chdir("..")
# Storing build artifacts
print("Storing build artifacts for", linux_version)
# Because Python doesn't support brace-fnmatch globbing
extensions = ['buildinfo', 'changes', 'dsc', 'deb', 'tar.xz']
extensions = ["buildinfo", "changes", "dsc", "deb", "tar.xz"]
artifacts = []
for extension in extensions:
artifacts.extend(Path(".").glob(f"*.{extension}"))
Expand Down
36 changes: 18 additions & 18 deletions grsecurity-urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import logging
import os
import re
import requests
import subprocess
import sys
from requests.auth import HTTPBasicAuth
import tempfile
from pathlib import Path

import requests
from requests.auth import HTTPBasicAuth

GRSECURITY_PATCH_TYPES = [
# stable6 corresponds to the long-term 5.15 kernel, good until Q4 2025
Expand Down Expand Up @@ -39,8 +41,7 @@ def parse_args():
default=False,
help="Dump kernel version required for specified patch type, then exit",
)
args = parser.parse_args()
return args
return parser.parse_args()


class GrsecurityPatch:
Expand All @@ -55,14 +56,14 @@ def __init__(self, patch_type):
self.grsecurity_username = os.environ.get("GRSECURITY_USERNAME")
self.grsecurity_password = os.environ.get("GRSECURITY_PASSWORD")
self.requests_auth = HTTPBasicAuth(self.grsecurity_username, self.grsecurity_password)
self.tempdir = Path(tempfile.mkdtemp())

@property
def patch_name(self):
patch_name_url = "https://grsecurity.net/latest_{}_patch".format(self.patch_type)
r = requests.get(patch_name_url)
patch_name_url = f"https://grsecurity.net/latest_{self.patch_type}_patch"
r = requests.get(patch_name_url) # noqa: S113
r.raise_for_status()
patch_name = r.content.rstrip().decode("utf-8")
return patch_name
return r.content.rstrip().decode("utf-8")

@property
def kernel_version(self):
Expand All @@ -84,23 +85,21 @@ def kernel_version(self):

@property
def patch_url(self):
patch_url = self.download_prefix + self.patch_name
return patch_url
return self.download_prefix + self.patch_name

@property
def patch_content(self):
patch_file = "/tmp/" + self.patch_name
patch_file = self.tempdir / self.patch_name
with open(patch_file) as f:
patch_content = f.read()
return patch_content
return f.read()

def download(self):
"""
Downloads patch and verifies it.
"""
for fname in [self.patch_name, self.patch_name + ".sig"]:
url = self.download_prefix + fname
dest_file = "/tmp/" + fname
dest_file = self.tempdir / fname
if os.path.exists(dest_file):
continue
download_file(url, dest_file, auth=self.requests_auth)
Expand All @@ -110,9 +109,9 @@ def verify(self):
Performs gpg verification of the detached signature file
for the patch. Assumes public key is already present in keyring.
"""
patch_file = "/tmp/" + self.patch_name
sig_file = patch_file + ".sig"
cmd = "gpgv --keyring /pubkeys/spender.gpg {} {}".format(sig_file, patch_file).split()
patch_file = self.tempdir / self.patch_name
sig_file = f"{patch_file}.sig"
cmd = f"/usr/bin/gpgv --keyring /pubkeys/spender.gpg {sig_file} {patch_file}".split()
with open(os.devnull, "w") as f:
subprocess.check_call(cmd, stdout=f, stderr=f)

Expand All @@ -122,7 +121,7 @@ def download_file(url, dest_file, auth=None):
Substitues for curl. Does not clobber files.
"""
if not os.path.exists(dest_file):
with requests.get(url, stream=True, auth=auth) as r:
with requests.get(url, stream=True, auth=auth) as r: # noqa: S113
r.raise_for_status()
with open(dest_file, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
Expand All @@ -146,6 +145,7 @@ def main():
logging.debug("Verifying grsecurity patch")
grsec_config.verify()
print(grsec_config.patch_content)
return 0


if __name__ == "__main__":
Expand Down
55 changes: 55 additions & 0 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[project]
name = "kernel-builder"
version = "0.1.0"
description = ""
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
]

[tool.poetry]
package-mode = false

[tool.poetry.group.dev.dependencies]
ruff = "^0.11.8"
zizmor = "^1.6.0"

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = [
# pycodestyle errors
"E",
# pyflakes
"F",
# isort
"I",
# flake8-gettext
"INT",
# flake8-pie
"PIE",
# pylint
"PL",
# flake8-pytest-style
"PT",
# flake8-pyi
"PYI",
# flake8-return
"RET",
# flake8-bandit
"S",
# flake8-simplify
"SIM",
# pyupgrade
"UP",
# pycodestyle warnings
"W",
# Unused noqa directive
"RUF100",
]
ignore = [
# Find contextlib.suppress() is harder to read
"SIM105",
# Find ternary statements harder to read
"SIM108",
# Flags any subprocess use
"S603",
]

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"