chore(deps): sync fork-compatible helper updates - #60
Conversation
…ate#320) Co-authored-by: Renovate Bot <renovatebot@non-existent-email.com>
…create#319) Co-authored-by: Renovate Bot <renovatebot@non-existent-email.com>
Verify locked artifacts with exact hash, size, and OpenSSH signatures before hostile-archive extraction. Revalidate cache and installs, publish atomically, and execute direct tool calls from post-verified sealed file descriptors. Co-Authored-By: ruflo-bot <ruflo-bot@users.noreply.github.com>
Batch enabled executable acquisition before legacy downloads, reject legacy acquisition paths, clear stale disabled bindings, and route PixeneOS-owned avbroot calls through sealed descriptor execution. Co-Authored-By: ruflo-bot <ruflo-bot@users.noreply.github.com>
Describe sealed direct execution, compatibility-only helper PATH resolution, deterministic reports, and the remaining trusted-runner gate before real OTA integration. Co-Authored-By: ruflo-bot <ruflo-bot@users.noreply.github.com>
Pin the maintained 0cwa helper after its upstream uv sync, update module versions, and install helper dependencies from pyproject.toml. Co-Authored-By: ruflo-bot <ruflo-bot@users.noreply.github.com>
📝 WalkthroughWalkthroughThe change adds reusable ROM build workflows, GrapheneOS and LineageOS provider support, output and publication policies, authenticated executable-tool bootstrapping, locked F-Droid integration, and extensive validation coverage. ChangesROM build and workflow orchestration
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 47
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/declarations.sh (1)
74-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGive
AFSR,AVBROOT,CUSTOTA_TOOL, andMY_AVBROOT_SETUPthe same environment-override form.Lines 76-87 change the module flags to
${ADDITIONALS_X:-${ADDITIONALS[X]:-default}}. Lines 74 and 93-95 keep the old single-level form, so those flags cannot be driven from the CI environment while their neighbours can. The inconsistency is easy to trip over when a new workflow input is added. Also, the trailing comment on line 96 reads "My AVBRoot setup", which duplicates line 95 and does not describeMAS_COMPATIBLE_SEPOLICY.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/declarations.sh` around lines 74 - 96, Update the ADDITIONALS[AFSR], ADDITIONALS[AVBROOT], ADDITIONALS[CUSTOTA_TOOL], and ADDITIONALS[MY_AVBROOT_SETUP] assignments to use the same environment-variable override fallback pattern as the neighboring flags, preserving their current defaults. Correct the trailing comment for ADDITIONALS[MAS_COMPATIBLE_SEPOLICY] so it describes the compatible SELinux policy setting rather than duplicating the MY_AVBROOT_SETUP description.src/util_functions.sh (1)
438-443: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick winPropagate the patch failure.
python "${my_avbroot_setup}/patch.py"runs without a status check.deactivatethen runs at line 443 and its status becomes the return value ofpatch_ota. A failed patch is reported as success, andcreate_otacontinues to release a missing or stale artifact. Every other tool invocation added in this change uses|| return 1.🐛 Proposed fix
# Python command to run the patch script - python "${my_avbroot_setup}/patch.py" "${args[@]}" + if ! python "${my_avbroot_setup}/patch.py" "${args[@]}"; then + deactivate + return 1 + fi fi🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/util_functions.sh` around lines 438 - 443, Update the patch.py invocation in patch_ota to immediately propagate a nonzero exit status, using the same || return 1 pattern as the other tool invocations. Keep deactivate after the successful patch so patch_ota cannot return its status or allow create_ota to continue after patch failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/build-rom.yml:
- Around line 250-253: Serialize gh-pages publication in the reusable workflow
by adding a concurrency group keyed to the gh-pages branch, preventing
simultaneous jobs from reaching the git push in the release publication block.
Keep the existing commit and push behavior unchanged once the job acquires the
group.
- Around line 103-109: Update the “Install Rust stable” step to pin
dtolnay/rust-toolchain to a full immutable commit SHA instead of the mutable
master reference, while preserving the stable toolchain configuration.
- Around line 245-249: Update the force-publish block in the workflow to write
its marker to a distinct file path inside the published tree, rather than the
`.tmp` directory used by WORKDIR. Use the same new marker path for both the
printf redirection and the subsequent git add operation.
- Around line 116-127: Update the “Setup signing environment” step to pass each
signing secret through the step’s env configuration instead of expanding secrets
inside the bash script, and write all GITHUB_ENV entries using one generated
unique heredoc delimiter rather than the fixed EOF marker. Preserve the existing
KEYS_AVB_BASE64, KEYS_CERT_OTA_BASE64, and KEYS_OTA_BASE64 variable names and
secret contents while consolidating the environment-file writes to avoid the
SC2129 warning.
- Around line 111-114: Update the Install Python step using actions/setup-python
in the signing job to replace the floating "3.12-dev" version with a stable
released Python version, preferably an exact patch version such as "3.12.11" for
reproducible builds.
In @.github/workflows/phase3-build-only.yml:
- Around line 37-42: Update the calling workflow’s permissions for the reusable
build-only job so its contents scope allows write access required by the called
workflow’s build job. Change the permissions block adjacent to the build-only
job from read-only to contents: write, preserving the existing reusable workflow
invocation.
In @.github/workflows/release.yml:
- Line 53: Confirm the intended scheduled device in the release workflow,
focusing on the device-id expression and the corresponding dispatch default
validated by tests/phase3_workflows_test.sh. If the divergence is unintentional,
align the scheduled path with the existing shiba default, matching the LineageOS
workflow’s behavior; otherwise document or preserve the intentional bramble
selection.
In @.github/workflows/renovate.yml:
- Line 28: Update the GitHub Actions uses reference for
renovatebot/github-action to the specified full commit SHA
316d7cd859606d6039a2182b7d69199e9b036835, replacing the mutable v46.2.1 tag
while leaving the existing token configuration unchanged.
In `@docs/fdroid-privileged-extension.md`:
- Around line 10-22: Update the documentation around
FDROID_PRIVILEGED_EXTENSION_CACHE to explicitly state that its default path is
.tmp/locked-artifacts when WORKDIR=.tmp, while preserving the existing
explanation of the optional cache override and TOML configuration keys.
In `@locks/executable-tools-v1.json`:
- Around line 68-92: Update the custota-tool lock entry in
executable-tools-v1.json from Custota v6.2 to v6.4 so it matches
VERSION[CUSTOTA] in src/declarations.sh. Replace the release URL, artifact
metadata, checksums, signature, size, and version with the values for the v6.4
x86_64 archive.
In `@src/bootstrap_archive.py`:
- Around line 29-30: Update the terminating helpers to use NoReturn so type
checkers recognize that they always raise: in src/bootstrap_archive.py lines
29-30, change _fail to -> NoReturn and import NoReturn; in src/bootstrap_io.py
lines 25-26 and src/bootstrap_executable_tools.py lines 59-60, change fail to ->
NoReturn and import NoReturn.
- Around line 429-431: Restructure the generator around the preparation logic so
the OSError handler covers only descriptor preparation, not the subsequent
yield. In the function containing yield descriptor, ensure descriptor is closed
on both preparation failure and consumer/execve failure paths, using the
existing cleanup pattern or an ExitStack, while preserving the sealed executable
failure behavior and propagating consumer OSError with its original errno.
- Around line 406-417: The sealing flow around required_seals and
requested_seals must treat F_SEAL_EXEC as optional for older kernels. Apply
required_seals first, then separately attempt F_SEAL_EXEC when available,
ignoring the unavailable-kernel OSError while preserving other failures;
validate applied_seals against the required seals and include F_SEAL_EXEC only
when successfully applied.
In `@src/bootstrap_executable_tools.py`:
- Around line 350-360: Update resolve to look up tool_id in tool["layout"] using
the same layout-member validation performed by run, and fail closed when the
member is absent instead of constructing installed / tool_id unconditionally.
Preserve the existing installation validation and digest-bound absolute-path
checks, while returning the executable path derived from the validated locked
layout member.
- Around line 67-79: Replace the denylist logic in sanitized_environment with a
confirmed allowlist containing only the environment variables required by
avbroot, afsr, and custota-tool. Validate the exact required set before
narrowing the output, preserve the existing type and unsafe-entry validation,
and exclude all unapproved variables by default.
- Around line 210-217: Update the receipt fast path in the bootstrap method
containing _receipt and _verify_pair so missing cached archive or signature
objects are treated as a cache miss: check both object paths exist before
calling _verify_pair, and fall through to the existing download and validation
flow when either is absent. Continue calling _verify_pair and failing closed
when both objects exist but verification fails, and update the corresponding
cache behavior statement in the executable-tool trust documentation.
- Around line 305-314: Update the rename exception handler in the tool-install
loop to catch OSError conflicts for both errno.EEXIST and errno.ENOTEMPTY, while
allowing unrelated OSError values to propagate. Reuse the existing
validate_install(installed, tool["layout"]) recovery path for either conflict.
In `@src/bootstrap_io.py`:
- Around line 80-100: Update the Git trust-verification subprocesses around
repository discovery, committed blob retrieval, and the corresponding later
verification call to use an explicit sanitized environment rather than
inheriting ambient variables, and provide a finite timeout for each invocation.
Catch subprocess.TimeoutExpired and convert it into the existing
BootstrapError/fail handling path, preserving the current validation behavior
for successful and failed Git commands.
- Around line 29-36: Update ensure_private_directory to create each missing path
component explicitly with mode 0o700 rather than using mkdir(parents=True).
Preserve existing-directory handling, then validate every component as a private
real directory so the entire bootstrap cache path maintains the documented
permissions.
In `@src/declarations.sh`:
- Around line 63-69: Record a follow-up issue to rename the misleading
GRAPHENEOS compatibility array and related references to a ROM-neutral name.
Retain the existing GRAPHENEOS keys and behavior in src/declarations.sh for now,
while tracking updates needed across the OTA fetcher, verifier, workflow
environment exports, and phase 3 acquisition tests.
In `@src/fetcher.sh`:
- Around line 83-95: Recompute the ota path after get_latest_version resolves
GRAPHENEOS[OTA_TARGET], before the download check, so downloads and
verify_rom_ota_digest use the resolved filename. In the same download block,
check curl’s exit status and fail immediately when the download fails instead of
proceeding to verification.
In `@src/ota_providers.sh`:
- Around line 19-22: Update the GrapheneOS version flow around
VERSION[GRAPHENEOS] and the OTA target construction so GRAPHENEOS_VERSION cannot
desynchronize the release metadata from the downloaded OTA: either remove the
override and consistently use validated latest_version, or validate the override
and use the resulting version for VERSION[GRAPHENEOS], GRAPHENEOS[OTA_TARGET],
and GRAPHENEOS[OTA_URL]. Apply the same fix to the corresponding LineageOS logic
near its VERSION assignment.
- Around line 8-9: Validate GRAPHENEOS[UPDATE_CHANNEL] and
GRAPHENEOS[UPDATE_TYPE] in resolve_rom_profile before either value is used in
request paths or related profile resolution. Apply the same strict
allowlist-style validation used for other untrusted path components, rejecting
values containing path or query separators and traversal syntax while preserving
valid channel and update-type values.
In `@src/rom_profiles.sh`:
- Around line 62-67: Update resolve_rom_profile so the _require_profile_boolean
validation for ADDITIONALS_MAS_COMPATIBLE_SEPOLICY immediately propagates a
nonzero result, preventing execution from continuing to
ROM_PROFILE_CLEAR_VBMETA_FLAGS and ensuring invalid input causes
resolve_rom_profile to fail.
- Around line 111-119: Update _locked_input_digest to capture sha256sum’s output
and validate its success before returning the digest. Ensure missing or invalid
input paths cause the function to return nonzero with no empty digest, so
callers’ existing || handlers execute.
- Around line 165-181: Update the MODULE_SELECTION_FINGERPRINT preimage to
include GRAPHENEOS[UPDATE_CHANNEL] and GRAPHENEOS[UPDATE_TYPE], alongside the
existing ROM, output, module, and helper inputs. Use the same values consumed by
generate_ota_info so builds differing only by update channel or type receive
distinct fingerprints and metadata filenames.
In `@src/util_functions.sh`:
- Around line 327-330: Propagate failures from the fail-closed helpers: in
src/util_functions.sh lines 327-330, update the resolve_rom_profile call in
patch_ota to return 1 on failure; at lines 739-753, update both
validate_device_name and module_selection_fingerprint in generate_ota_info to
return 1 when they fail, preventing subsequent output composition.
- Around line 45-50: Define a shared LOCKED_EXECUTABLE_TOOLS array containing
afsr, avbroot, and custota-tool, then replace each hardcoded tool-list check in
the affected utility logic—including the executable filtering block and the
other three sites—with membership checks against that array. Ensure all
locked-tool behavior remains unchanged while allowing future tools to be added
in one place.
- Around line 367-368: Update the existing-output check near the
ADDITIONALS[FDROID_PRIVILEGED_EXTENSION] condition so the patched OTA glob
expands instead of being passed literally to ls. Align the checked pattern with
the actual output filename assigned to OUTPUTS[PATCHED_OTA], correcting the
.pathed versus .patched spelling mismatch, or remove the check if the
fingerprinted output naming makes reuse impossible.
- Around line 398-404: Remove the unreachable `elif !
prepare_fdroid_privileged_extension args "${my_avbroot_setup}"` branch from the
module-argument setup, leaving the `FDROID_PRIVILEGED_EXTENSION == 'true'` path
that appends `locked_module_args` unchanged.
- Around line 344-347: Update the virtual-environment check around enable_venv
to use a nounset-safe default-value expansion for VIRTUAL_ENV, so an unset
variable is treated as empty and still activates the environment. Preserve the
existing behavior when VIRTUAL_ENV is already set.
- Around line 545-546: Update enable_venv so it returns a nonzero status
whenever the virtual-environment activation script is missing or activation does
not occur, while preserving successful activation’s zero status. Keep the
existing enable_venv || return 1 guard in env_setup so setup stops before
subsequent installation and Python commands when activation fails.
- Around line 105-131: Update bootstrap_executable_tools,
resolve_executable_tool, and run_executable_tool to invoke
src/bootstrap_executable_tools.py via an absolute repository-root-anchored path
instead of relying on the current working directory. Apply the same new literal
path in tests/executable_tool_routing_test.sh at the referenced assertion.
- Around line 709-712: Validate the extracted AVB public-key bytes in the
pipeline around avb_pkmd.bin before treating extraction as successful. Ensure
missing public_key matches, empty sed output, or a zero-byte output file causes
the enclosing function to return 1, while preserving the existing extraction
behavior for valid keys.
In `@src/validate_executable_tool_lock.py`:
- Around line 87-88: Update the return annotation of fail to typing.NoReturn,
adding the necessary typing import if absent, so type checkers recognize that
every call terminates by raising ValidationError and correctly narrow callers
such as require_string and read_regular_file.
In `@tests/executable_tool_bootstrap_test.py`:
- Around line 452-455: Update the NUL-argument test around bootstrapper.run to
patch fd_exec_supported as True while asserting BootstrapError, ensuring the
failure is caused by the NUL argument rather than unsupported exec. Keep the
existing os.execve mock and execve.assert_not_called() checks unchanged.
In `@tests/executable_tool_lock_test.sh`:
- Around line 150-153: Add a noncanonical_json_form case to the cases array in
test_invalid_locks_fail_closed that writes the unchanged lock JSON using a
different indentation or without sort_keys, then assert the validator rejects
it.
In `@tests/module_selection_test.sh`:
- Around line 217-230: Replace the short-circuit append in
remove_expected_modules with an explicit if block that appends filtered values
only when remove is false. Preserve the existing filtering logic and ensure the
conditional block does not return a non-zero status under set -e when a value is
removed.
- Around line 501-508: Update
test_fdroid_enabled_does_not_reuse_legacy_output_marker to call
enable_fdroid_fixture after reset_fixture, providing valid locked F-Droid inputs
before creating the legacy marker. Then adjust its assertions to expect the
actual patch_ota outcome when the marker is ignored, ensuring the test reaches
marker handling rather than failing validation in
verify_fdroid_privileged_extension_inputs.
In `@tests/phase3_provider_acquisition_test.sh`:
- Around line 72-117: Expand the JSON fixture returned by curl in
test_lineageos_v2_metadata to include a second, older nightly build before the
current build, with distinct date, filename, URL, and SHA-256 values. Keep the
existing assertions verifying that the newest build’s filename, URL, version,
and checksum are selected, thereby exercising fetch_lineageos_ota_metadata’s
ordering behavior.
In `@tests/phase3_rom_contract_test.sh`:
- Around line 76-83: Add a test alongside test_unknown_rom_fails_closed that
sets ADDITIONALS_MAS_COMPATIBLE_SEPOLICY to “yes”, invokes resolve_rom_profile,
and asserts a nonzero status, preserving the existing unsupported-ROM test
unchanged.
In `@tests/phase3_workflows_test.sh`:
- Around line 152-161: Extend test_manual_build_only_acceptance to verify that
the discovered caller of build-rom.yml grants contents: write, in addition to
requiring workflow_dispatch. Use the existing acceptance workflow content and
assertion helpers so the test enforces the reusable workflow permission
contract.
In `@tests/test_bootstrap_archive.py`:
- Around line 410-412: Move the closed-descriptor assertion into the scope
immediately after open_sealed_executable closes the descriptor, before
open_validated_executable, installed_fixture, or temporary-directory teardown
can open other descriptors. Assert closure with fcntl.F_GETFD, ensuring the
inner mock.patch on bootstrap_archive.fcntl.fcntl has been exited or using the
unpatched fcntl module.
- Around line 414-432: Update test_sealed_memfd_executes_a_real_native_binary to
locate the native true binary using /usr/bin/true with /bin/true as a fallback,
and skip the test if neither exists. Also skip when os.execve is not present in
os.supports_fd before forking, preserving the existing execution assertions when
both prerequisites are available.
In `@tests/verifier_test.sh`:
- Around line 75-83: Extend tests/verifier_test.sh with a test covering
executable-tool rejection through verify_downloads for afsr, avbroot, and
custota-tool. Assert each fails without invoking auto_retry_check, while
preserving the existing signature and unsigned-exemption coverage.
- Around line 107-114: Update the fixture repository setup around the git
init/config/commit commands to explicitly disable commit signing and global
hooks for all repository operations. Configure the fixture’s local Git settings,
or pass equivalent per-command options, before the add and commit steps so
global commit.gpgsign and core.hooksPath cannot affect the test.
- Around line 94-98: Extend
test_locked_input_rejects_worktree_index_mode_and_path_aliases to create an
untracked regular file inside its fixture Git repository and pass that path
through verify_fdroid_privileged_extension_inputs, asserting verification fails
specifically for the untracked input. Do not use the external TEST_ROOT
temporary paths for this case, so verify_checked_in_locked_input reaches the git
ls-files validation branch.
---
Outside diff comments:
In `@src/declarations.sh`:
- Around line 74-96: Update the ADDITIONALS[AFSR], ADDITIONALS[AVBROOT],
ADDITIONALS[CUSTOTA_TOOL], and ADDITIONALS[MY_AVBROOT_SETUP] assignments to use
the same environment-variable override fallback pattern as the neighboring
flags, preserving their current defaults. Correct the trailing comment for
ADDITIONALS[MAS_COMPATIBLE_SEPOLICY] so it describes the compatible SELinux
policy setting rather than duplicating the MY_AVBROOT_SETUP description.
In `@src/util_functions.sh`:
- Around line 438-443: Update the patch.py invocation in patch_ota to
immediately propagate a nonzero exit status, using the same || return 1 pattern
as the other tool invocations. Keep deactivate after the successful patch so
patch_ota cannot return its status or allow create_ota to continue after patch
failure.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b006b1ed-4a4a-4f73-a6db-7117bdb7bcc1
📒 Files selected for processing (37)
.github/renovate.json5.github/workflows/build-rom.yml.github/workflows/phase3-build-only.yml.github/workflows/release-lineage.yml.github/workflows/release.yml.github/workflows/renovate.yml.gitleaks.toml.gitleaksignoreREADME.mddocs/executable-tool-trust.mddocs/fdroid-privileged-extension.mdlocks/executable-tools-v1.jsonsrc/bootstrap_archive.pysrc/bootstrap_executable_tools.pysrc/bootstrap_io.pysrc/declarations.shsrc/fetcher.shsrc/main.shsrc/ota_providers.shsrc/rom_profiles.shsrc/scan_secrets.shsrc/util_functions.shsrc/validate_executable_tool_lock.pysrc/verifier.shtests/declarations_config_test.shtests/executable_tool_bootstrap_test.pytests/executable_tool_bootstrap_test.shtests/executable_tool_lock_test.shtests/executable_tool_routing_test.shtests/module_selection_test.shtests/phase3_provider_acquisition_test.shtests/phase3_rom_contract_test.shtests/phase3_workflows_test.shtests/secrets_scan_test.shtests/test_bootstrap_archive.pytests/verifier_test.shtrust/chenxiaolong.allowed_signers
| - name: Install Rust stable | ||
| uses: dtolnay/rust-toolchain@master | ||
| with: | ||
| toolchain: stable 2 weeks ago | ||
|
|
||
| - name: Cache Rust dependencies | ||
| uses: Swatinem/rust-cache@v2.9.2 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Pin dtolnay/rust-toolchain to a commit SHA.
Line 104 references the mutable master branch. This job holds AVB_KEY, OTA_KEY, and both passphrases, and it has contents: write. Any change pushed to that branch runs with those secrets. Pin the action to a full commit SHA.
🔒 Proposed fix
- name: Install Rust stable
- uses: dtolnay/rust-toolchain@master
+ uses: dtolnay/rust-toolchain@<full-commit-sha> # stable
with:
toolchain: stable 2 weeks ago🧰 Tools
🪛 zizmor (1.29.0)
[error] 104-104: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 109-109: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 109-109: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default
(cache-poisoning)
[info] 104-104: action functionality is already included by the runner (superfluous-actions): use rustup and/or cargo in a script step
(superfluous-actions)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-rom.yml around lines 103 - 109, Update the “Install
Rust stable” step to pin dtolnay/rust-toolchain to a full immutable commit SHA
instead of the mutable master reference, while preserving the stable toolchain
configuration.
Source: Linters/SAST tools
| - name: Install Python | ||
| uses: actions/setup-python@v7 | ||
| with: | ||
| python-version: "3.12-dev" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
actions/setup-python python-version 3.12-dev prerelease behavior
💡 Result:
In the actions/setup-python action, the x.y-dev syntax (such as 3.12-dev) is a specific notation used to automatically install the latest available patch version of a given minor release, which explicitly includes alpha, beta, and release candidate (pre-release) versions [1][2]. Key behaviors regarding this syntax and pre-release handling include: 1. Automatic Inclusion: Unlike standard version specifiers (e.g., 3.12), the 3.y-dev notation is designed to include pre-release versions for the specified minor version automatically, without requiring any additional configuration [1][2]. 2. Purpose of allow-prereleases: The separate allow-prereleases input (which defaults to false) serves a different purpose [1][3]. It allows the action to fall back to a pre-release version of Python only when a matching General Availability (GA) version does not yet exist for a specified minor version (e.g., specifying python-version: 3.14 with allow-prereleases: true when 3.14 GA has not been released) [1][4]. 3. Interaction: The x.y-dev syntax is distinct from the fallback behavior controlled by allow-prereleases [1][4]. You do not need to set allow-prereleases: true to use the x.y-dev syntax to fetch the latest pre-release for that minor version [1][2]. In summary, if you use 3.12-dev, the action will resolve to the latest patch or pre-release available for the 3.12 series by default [1][2]. If you use 3.12 (without -dev), it will only install a stable GA release unless you explicitly set allow-prereleases: true [1][3][4].
Citations:
- 1: https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md
- 2: https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md?rgh-link-date=2025-09-21T10%3A23%3A30.000Z
- 3: https://github.com/actions/setup-python/releases/tag/v4.6.0
- 4: setup-python@v6 with python-version: "3.x" installs pre-release Python 3.14.2 instead of latest stable version actions/setup-python#1275
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Workflow relevant sections:"
if [ -f .github/workflows/build-rom.yml ]; then
nl -ba .github/workflows/build-rom.yml | sed -n '1,180p'
else
echo "workflow file missing"
fi
echo
echo "Check whether 3.12-dev is an in-development prerelease in current pythons repository:"
python3 - <<'PY'
from urllib.request import urlopen, Request
import json
versions = json.load(urlopen(Request("https://raw.githubusercontent.com/python/cpython/main/versions.json", headers={"User-Agent":"README-verification"})))
print("versions:", versions)
PYRepository: 0cwa/PixeneOS
Length of output: 219
🏁 Script executed:
#!/bin/bash
set -e
export PYTHONUNBUFFERED=1
echo "Workflow file exists:"
test -f .github/workflows/build-rom.yml && echo yes || echo no
echo
echo "build-rom.yml relevant lines:"
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/build-rom.yml")
print("exists?", p.exists())
if p.exists():
lines = p.read_text().splitlines()
for i in range(max(0, 105), min(len(lines), 130)):
print(f"{i+1:4}: {lines[i]}")
PY
echo
echo "Releases/build steps and Python usage:"
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/build-rom.yml")
if p.exists():
lines = p.read_text().splitlines()
for needle in ["release", "sign", "build", "python", "python-version"]:
matches = [(i+1, line) for i, line in enumerate(lines) if needle.lower() in line.lower()]
print(needle + ":")
for n, line in matches[:30]:
print(f"{n:4}: {line}")
PY
echo
echo "Check python x.y-dev in CPython versions.json:"
python3 - <<'PY'
from urllib.request import urlopen, Request
import json
try:
versions = json.load(urlopen(Request("https://raw.githubusercontent.com/python/cpython/main/versions.json", headers={"User-Agent":"README-verification"})))
except Exception as exc:
print("FETCH_ERROR", type(exc).__name__, str(exc))
raise
print("python_version:", versions.get("python_version"))
print("versions:", versions.get("versions"))
PYRepository: 0cwa/PixeneOS
Length of output: 5596
Pin the Python interpreter for the signing path.
This job installs Python 3.12-dev just before setting up the signing environment. A dev release resolver can return a different interpreter patch over time, so use an exact released version such as "3.12" or "3.12.11" if this needs to be reproducible.
🧰 Tools
🪛 zizmor (1.29.0)
[error] 112-112: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-rom.yml around lines 111 - 114, Update the Install
Python step using actions/setup-python in the signing job to replace the
floating "3.12-dev" version with a stable released Python version, preferably an
exact patch version such as "3.12.11" for reproducible builds.
| - name: Setup signing environment | ||
| shell: bash | ||
| run: | | ||
| echo "KEYS_AVB_BASE64<<EOF" >> "${GITHUB_ENV}" | ||
| echo "${{ secrets.AVB_KEY }}" >> "${GITHUB_ENV}" | ||
| echo "EOF" >> "${GITHUB_ENV}" | ||
| echo "KEYS_CERT_OTA_BASE64<<EOF" >> "${GITHUB_ENV}" | ||
| echo "${{ secrets.CERT_OTA }}" >> "${GITHUB_ENV}" | ||
| echo "EOF" >> "${GITHUB_ENV}" | ||
| echo "KEYS_OTA_BASE64<<EOF" >> "${GITHUB_ENV}" | ||
| echo "${{ secrets.OTA_KEY }}" >> "${GITHUB_ENV}" | ||
| echo "EOF" >> "${GITHUB_ENV}" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Pass the signing secrets through env and use a unique heredoc delimiter.
Lines 119-127 expand secrets directly into the script body. The heredoc delimiter is the fixed string EOF. If any secret ever contains a line equal to EOF, the block terminates early and the remaining secret content is written to GITHUB_ENV as attacker-influenced variable assignments. Referencing the secrets through env and generating a random delimiter removes both the template expansion and the delimiter collision.
🔒 Proposed fix
- name: Setup signing environment
shell: bash
+ env:
+ AVB_KEY: ${{ secrets.AVB_KEY }}
+ CERT_OTA: ${{ secrets.CERT_OTA }}
+ OTA_KEY: ${{ secrets.OTA_KEY }}
run: |
- echo "KEYS_AVB_BASE64<<EOF" >> "${GITHUB_ENV}"
- echo "${{ secrets.AVB_KEY }}" >> "${GITHUB_ENV}"
- echo "EOF" >> "${GITHUB_ENV}"
- echo "KEYS_CERT_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
- echo "${{ secrets.CERT_OTA }}" >> "${GITHUB_ENV}"
- echo "EOF" >> "${GITHUB_ENV}"
- echo "KEYS_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
- echo "${{ secrets.OTA_KEY }}" >> "${GITHUB_ENV}"
- echo "EOF" >> "${GITHUB_ENV}"
+ delimiter="$(openssl rand -hex 16)"
+ {
+ printf '%s<<%s\n%s\n%s\n' KEYS_AVB_BASE64 "${delimiter}" "${AVB_KEY}" "${delimiter}"
+ printf '%s<<%s\n%s\n%s\n' KEYS_CERT_OTA_BASE64 "${delimiter}" "${CERT_OTA}" "${delimiter}"
+ printf '%s<<%s\n%s\n%s\n' KEYS_OTA_BASE64 "${delimiter}" "${OTA_KEY}" "${delimiter}"
+ } >> "${GITHUB_ENV}"This also resolves the SC2129 shellcheck finding reported by actionlint on this step.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Setup signing environment | |
| shell: bash | |
| run: | | |
| echo "KEYS_AVB_BASE64<<EOF" >> "${GITHUB_ENV}" | |
| echo "${{ secrets.AVB_KEY }}" >> "${GITHUB_ENV}" | |
| echo "EOF" >> "${GITHUB_ENV}" | |
| echo "KEYS_CERT_OTA_BASE64<<EOF" >> "${GITHUB_ENV}" | |
| echo "${{ secrets.CERT_OTA }}" >> "${GITHUB_ENV}" | |
| echo "EOF" >> "${GITHUB_ENV}" | |
| echo "KEYS_OTA_BASE64<<EOF" >> "${GITHUB_ENV}" | |
| echo "${{ secrets.OTA_KEY }}" >> "${GITHUB_ENV}" | |
| echo "EOF" >> "${GITHUB_ENV}" | |
| - name: Setup signing environment | |
| shell: bash | |
| env: | |
| AVB_KEY: ${{ secrets.AVB_KEY }} | |
| CERT_OTA: ${{ secrets.CERT_OTA }} | |
| OTA_KEY: ${{ secrets.OTA_KEY }} | |
| run: | | |
| delimiter="$(openssl rand -hex 16)" | |
| { | |
| printf '%s<<%s\n%s\n%s\n' KEYS_AVB_BASE64 "${delimiter}" "${AVB_KEY}" "${delimiter}" | |
| printf '%s<<%s\n%s\n%s\n' KEYS_CERT_OTA_BASE64 "${delimiter}" "${CERT_OTA}" "${delimiter}" | |
| printf '%s<<%s\n%s\n%s\n' KEYS_OTA_BASE64 "${delimiter}" "${OTA_KEY}" "${delimiter}" | |
| } >> "${GITHUB_ENV}" |
🧰 Tools
🪛 actionlint (1.7.12)
[error] 118-118: shellcheck reported issue in this script: SC2129:style:1:1: Consider using { cmd1; cmd2; } >> file instead of individual redirects
(shellcheck)
🪛 zizmor (1.29.0)
[warning] 120-120: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 123-123: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 126-126: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-rom.yml around lines 116 - 127, Update the “Setup
signing environment” step to pass each signing secret through the step’s env
configuration instead of expanding secrets inside the bash script, and write all
GITHUB_ENV entries using one generated unique heredoc delimiter rather than the
fixed EOF marker. Preserve the existing KEYS_AVB_BASE64, KEYS_CERT_OTA_BASE64,
and KEYS_OTA_BASE64 variable names and secret contents while consolidating the
environment-file writes to avoid the SC2129 warning.
Source: Linters/SAST tools
| if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then | ||
| printf 'force-publish run %s (attempt %s)\n' \ | ||
| "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > .tmp | ||
| git add -- .tmp | ||
| fi |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
.tmp collides with WORKDIR and breaks the force-publish path.
src/declarations.sh line 28 sets WORKDIR=".tmp", and the Patch OTA step exports WORKDIR to GITHUB_ENV, so .tmp exists in the workspace as a directory during this step. Line 247 redirects into .tmp, which fails with "Is a directory", and the step aborts. If the directory were ever removed, git add -- .tmp would instead commit the whole build work directory to gh-pages. Use a distinct marker path inside the published tree.
🐛 Proposed fix
if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then
+ marker="variants/${ROM_FAMILY}/${flavor}/.force-publish"
printf 'force-publish run %s (attempt %s)\n' \
- "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > .tmp
- git add -- .tmp
+ "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > "${marker}"
+ git add -- "${marker}"
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then | |
| printf 'force-publish run %s (attempt %s)\n' \ | |
| "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > .tmp | |
| git add -- .tmp | |
| fi | |
| if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then | |
| marker="variants/${ROM_FAMILY}/${flavor}/.force-publish" | |
| printf 'force-publish run %s (attempt %s)\n' \ | |
| "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > "${marker}" | |
| git add -- "${marker}" | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-rom.yml around lines 245 - 249, Update the
force-publish block in the workflow to write its marker to a distinct file path
inside the published tree, rather than the `.tmp` directory used by WORKDIR. Use
the same new marker path for both the printf redirection and the subsequent git
add operation.
| if ! git diff-index --quiet HEAD; then | ||
| git commit -m "release(${current_commit}): publish ${ROM_FAMILY} ${GRAPHENEOS_VERSION} ${MODULE_SELECTION_FINGERPRINT}" | ||
| git push origin gh-pages | ||
| fi |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
Serialize gh-pages publication or handle the push race.
The scheduled triggers run every six hours for both ROM families, and each caller can run concurrently for different devices. Two jobs that reach line 252 at the same time produce a non-fast-forward push, and the second job fails after a full build. Add a concurrency group keyed on the gh-pages branch to the reusable workflow, or retry the commit after git pull --rebase origin gh-pages.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-rom.yml around lines 250 - 253, Serialize gh-pages
publication in the reusable workflow by adding a concurrency group keyed to the
gh-pages branch, preventing simultaneous jobs from reaching the git push in the
release publication block. Keep the existing commit and push behavior unchanged
once the job acquires the group.
| test_manual_build_only_acceptance() { | ||
| local acceptance | ||
|
|
||
| acceptance="$(find_manual_acceptance_workflow)" || | ||
| fail "missing manual build-only workflow that calls build-rom.yml" | ||
| assert_contains \ | ||
| "${acceptance}" \ | ||
| 'workflow_dispatch:' \ | ||
| "acceptance workflow must be manually dispatched" | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Assert that every caller of build-rom.yml grants contents: write.
The reusable workflow declares contents: write on its build job. A caller that grants only contents: read fails at run start. .github/workflows/phase3-build-only.yml currently has that mismatch, and this suite does not detect it. Add the check to test_manual_build_only_acceptance so the contract is enforced by the tests.
💚 Proposed test addition
assert_contains \
"${acceptance}" \
'workflow_dispatch:' \
"acceptance workflow must be manually dispatched"
+ assert_contains \
+ "${acceptance}" \
+ 'contents:[[:space:]]*write' \
+ "callers of build-rom.yml must grant contents: write"
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test_manual_build_only_acceptance() { | |
| local acceptance | |
| acceptance="$(find_manual_acceptance_workflow)" || | |
| fail "missing manual build-only workflow that calls build-rom.yml" | |
| assert_contains \ | |
| "${acceptance}" \ | |
| 'workflow_dispatch:' \ | |
| "acceptance workflow must be manually dispatched" | |
| } | |
| test_manual_build_only_acceptance() { | |
| local acceptance | |
| acceptance="$(find_manual_acceptance_workflow)" || | |
| fail "missing manual build-only workflow that calls build-rom.yml" | |
| assert_contains \ | |
| "${acceptance}" \ | |
| 'workflow_dispatch:' \ | |
| "acceptance workflow must be manually dispatched" | |
| assert_contains \ | |
| "${acceptance}" \ | |
| 'contents:[[:space:]]*write' \ | |
| "callers of build-rom.yml must grant contents: write" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/phase3_workflows_test.sh` around lines 152 - 161, Extend
test_manual_build_only_acceptance to verify that the discovered caller of
build-rom.yml grants contents: write, in addition to requiring
workflow_dispatch. Use the existing acceptance workflow content and assertion
helpers so the test enforces the reusable workflow permission contract.
| self.assertEqual(len(captured), 1) | ||
| with self.assertRaises(OSError): | ||
| os.fstat(captured[0]) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
The closed-descriptor assertion can pass or fail for the wrong reason.
captured[0] holds a file-descriptor number, not an identity. open_sealed_executable closes it inside the with block. The assertion runs after open_validated_executable and installed_fixture have both torn down, and TemporaryDirectory cleanup opens directory handles during rmtree. If the runtime reuses that descriptor number, os.fstat succeeds and the test fails even though the code is correct.
Assert the closure at a point where no other descriptor can be opened, and use fcntl.F_GETFD so that the check states intent.
💚 Proposed fix to assert closure deterministically
with self.installed_fixture() as (directory, _):
layout = [locked_member()]
with open_validated_executable(directory, layout, "tool") as source:
with mock.patch("bootstrap_archive.os.memfd_create", capture_memfd):
with mock.patch(
"bootstrap_archive.fcntl.fcntl", side_effect=OSError("fail")
):
with self.assertRaises(BootstrapError):
with open_sealed_executable(source, layout[0]):
self.fail("unsealed executable was yielded")
- self.assertEqual(len(captured), 1)
- with self.assertRaises(OSError):
- os.fstat(captured[0])
+ self.assertEqual(len(captured), 1)
+ with self.assertRaises(OSError):
+ fcntl.fcntl(captured[0], fcntl.F_GETFD)Note that the inner mock.patch on bootstrap_archive.fcntl.fcntl must be exited before the fcntl.fcntl assertion, or the assertion must call the unpatched module directly.
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 411-411: Use pytest.raises instead of unittest-style assertRaises
Replace assertRaises with pytest.raises
(PT027)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_bootstrap_archive.py` around lines 410 - 412, Move the
closed-descriptor assertion into the scope immediately after
open_sealed_executable closes the descriptor, before open_validated_executable,
installed_fixture, or temporary-directory teardown can open other descriptors.
Assert closure with fcntl.F_GETFD, ensuring the inner mock.patch on
bootstrap_archive.fcntl.fcntl has been exited or using the unpatched fcntl
module.
| test_unsigned_exceptions_succeed() { | ||
| reset_fixture unsigned-helper | ||
| mkdir -p "${WORKDIR}/tools/my-avbroot-setup" | ||
| verify_downloads my-avbroot-setup >/dev/null | ||
|
|
||
| reset_fixture unsigned-magisk | ||
| touch "${WORKDIR}/modules/magisk.apk" | ||
| verify_downloads magisk >/dev/null | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find existing coverage for the executable-tool rejection in verify_downloads.
rg -n -C4 'verify_downloads' tests/Repository: 0cwa/PixeneOS
Length of output: 4635
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== verifier_test header =="
sed -n '1,95p' tests/verifier_test.sh
echo
echo "== verifier relevant sections =="
fd -a verifier.sh . | sed 's#^\./##' | while read -r f; do
echo "-- $f"
wc -l "$f"
sed -n '1,160p' "$f"
done
echo
echo "== executable_tool_routing relevant =="
sed -n '300,330p' tests/executable_tool_routing_test.shRepository: 0cwa/PixeneOS
Length of output: 8222
Add coverage for executable-tool rejection.
tests/verifier_test.sh covers signature success/failure and the unsigned exemptions, but it does not assert that afsr, avbroot, or custota-tool fail through verify_downloads without calling auto_retry_check. tests/executable_tool_routing_test.sh tests legacy verify_downloads stubs, so add this in the verifier suite.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/verifier_test.sh` around lines 75 - 83, Extend tests/verifier_test.sh
with a test covering executable-tool rejection through verify_downloads for
afsr, avbroot, and custota-tool. Assert each fails without invoking
auto_retry_check, while preserving the existing signature and unsigned-exemption
coverage.
| touch "${lock_path}" "${profile_path}" | ||
| if verify_fdroid_privileged_extension_inputs \ | ||
| "${lock_path}" "${profile_path}" >/dev/null 2>&1; then | ||
| fail "locked inputs: untracked temporary files unexpectedly succeeded" | ||
| fi |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
This case does not exercise the untracked-file branch.
${lock_path} and ${profile_path} live under TEST_ROOT, which mktemp -d places outside the repository. verify_checked_in_locked_input resolves the repository root from the current working directory, so these paths fail the containment check at src/verifier.sh lines 29-36. The git ls-files --error-unmatch branch at lines 38-39 never runs.
No test creates an untracked regular file inside a git repository. Add that case to test_locked_input_rejects_worktree_index_mode_and_path_aliases, which already builds a fixture repository.
💚 Proposed test addition in `test_locked_input_rejects_worktree_index_mode_and_path_aliases`
+ local untracked_path="${repository}/locks/untracked.json"
+ printf '%s\n' '{"fixture":"untracked"}' >"${untracked_path}"
+ if (
+ cd "${repository}"
+ verify_checked_in_locked_input "${untracked_path}"
+ ); then
+ fail "untracked in-repository lock unexpectedly succeeded"
+ fi
+ rm -f -- "${untracked_path}"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/verifier_test.sh` around lines 94 - 98, Extend
test_locked_input_rejects_worktree_index_mode_and_path_aliases to create an
untracked regular file inside its fixture Git repository and pass that path
through verify_fdroid_privileged_extension_inputs, asserting verification fails
specifically for the untracked input. Do not use the external TEST_ROOT
temporary paths for this case, so verify_checked_in_locked_input reaches the git
ls-files validation branch.
| git init -q -- "${repository}" | ||
| git -C "${repository}" config user.email test@example.invalid | ||
| git -C "${repository}" config user.name "Pixene test" | ||
| git -C "${repository}" config core.filemode true | ||
| mkdir -p "$(dirname -- "${lock_path}")" | ||
| printf '%s\n' '{"fixture":"clean"}' >"${lock_path}" | ||
| git -C "${repository}" add -- "locks/fdroid lock.json" | ||
| git -C "${repository}" commit -q -m fixture |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Isolate the fixture repository from global Git configuration.
git init and git commit inherit the developer's global configuration. A global commit.gpgsign = true makes line 114 fail. set -e then aborts the whole suite with no clear cause. Global hooks configured through core.hooksPath cause the same result.
Disable signing and hooks for the fixture commands.
♻️ Proposed fix
git init -q -- "${repository}"
git -C "${repository}" config user.email test@example.invalid
git -C "${repository}" config user.name "Pixene test"
git -C "${repository}" config core.filemode true
+ git -C "${repository}" config commit.gpgsign false
+ git -C "${repository}" config core.hooksPath /dev/null
mkdir -p "$(dirname -- "${lock_path}")"
printf '%s\n' '{"fixture":"clean"}' >"${lock_path}"
git -C "${repository}" add -- "locks/fdroid lock.json"
- git -C "${repository}" commit -q -m fixture
+ git -C "${repository}" commit -q --no-verify -m fixture📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| git init -q -- "${repository}" | |
| git -C "${repository}" config user.email test@example.invalid | |
| git -C "${repository}" config user.name "Pixene test" | |
| git -C "${repository}" config core.filemode true | |
| mkdir -p "$(dirname -- "${lock_path}")" | |
| printf '%s\n' '{"fixture":"clean"}' >"${lock_path}" | |
| git -C "${repository}" add -- "locks/fdroid lock.json" | |
| git -C "${repository}" commit -q -m fixture | |
| git init -q -- "${repository}" | |
| git -C "${repository}" config user.email test@example.invalid | |
| git -C "${repository}" config user.name "Pixene test" | |
| git -C "${repository}" config core.filemode true | |
| git -C "${repository}" config commit.gpgsign false | |
| git -C "${repository}" config core.hooksPath /dev/null | |
| mkdir -p "$(dirname -- "${lock_path}")" | |
| printf '%s\n' '{"fixture":"clean"}' >"${lock_path}" | |
| git -C "${repository}" add -- "locks/fdroid lock.json" | |
| git -C "${repository}" commit -q --no-verify -m fixture |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/verifier_test.sh` around lines 107 - 114, Update the fixture repository
setup around the git init/config/commit commands to explicitly disable commit
signing and global hooks for all repository operations. Configure the fixture’s
local Git settings, or pass equivalent per-command options, before the add and
commit steps so global commit.gpgsign and core.hooksPath cannot affect the test.
| required_seals = ( | ||
| fcntl.F_SEAL_WRITE | ||
| | fcntl.F_SEAL_GROW | ||
| | fcntl.F_SEAL_SHRINK | ||
| | fcntl.F_SEAL_SEAL | ||
| ) | ||
| optional_exec_seal = getattr(fcntl, "F_SEAL_EXEC", 0) | ||
| requested_seals = required_seals | optional_exec_seal | ||
| fcntl.fcntl(descriptor, fcntl.F_ADD_SEALS, requested_seals) | ||
| applied_seals = fcntl.fcntl(descriptor, fcntl.F_GET_SEALS) | ||
| if applied_seals & requested_seals != requested_seals: | ||
| _fail("sealed executable is missing required seals") |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Linux kernel version that added F_SEAL_EXEC memfd seal and Python version that exposes fcntl.F_SEAL_EXEC
💡 Result:
The F_SEAL_EXEC memfd seal was added to the Linux kernel in version 6.3 [1][2]. Regarding Python, while the fcntl module has included generic F_SEAL_* constants since Python 3.8 [3][4], it does not explicitly expose F_SEAL_EXEC as a named constant in its official documentation or library interface. Users requiring this functionality in Python typically define the constant manually using its integer value, which is 0x0020 [5][6][7].
Citations:
- 1: http://public-inbox.org/libc-alpha/20230824203625.1343061-1-kolyshkin@gmail.com/
- 2: tuliom/glibc@42c960a
- 3: https://docs.python.org/3/library/fcntl.html
- 4: https://docs.python.org/3.11/library/fcntl.html
- 5: https://docs.rs/syd/latest/src/syd/seal.rs.html
- 6: https://fossies.org/linux/runc/libcontainer/exeseal/cloned_binary_linux.go
- 7: https://shadow.github.io/docs/rust/linux_api/fcntl/enum.FcntlSealType.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate bootstrap_archive.py =="
fd -a 'bootstrap_archive.py' . || true
echo "== relevant file excerpt =="
if [ -f src/bootstrap_archive.py ]; then
nl -ba src/bootstrap_archive.py | sed -n '360,450p'
fi
echo "== search F_SEAL_EXEC / F_ADD_SEALS usage =="
rg -n "F_SEAL_EXEC|F_ADD_SEALS|F_GET_SEALS|required_seals|optional_exec_seal" .Repository: 0cwa/PixeneOS
Length of output: 348
🌐 Web query:
Linux kernel 6.3 F_SEAL_EXEC memfd seals F_ADD_SEALS EINVAL older kernel
💡 Result:
The F_SEAL_EXEC seal and the accompanying MFD_NOEXEC_SEAL and MFD_EXEC flags for memfd_create were introduced in Linux kernel 6.3 [1][2][3]. When attempting to use F_SEAL_EXEC or the new memfd_create flags on a kernel older than 6.3, the system will return EINVAL (Invalid argument) because the kernel does not recognize these constants [4][5]. Specifically: 1. F_SEAL_EXEC: If an application calls fcntl(fd, F_ADD_SEALS, F_SEAL_EXEC), the kernel will return EINVAL because F_SEAL_EXEC is an unrecognized sealing bit in older versions [5]. 2. MFD_NOEXEC_SEAL / MFD_EXEC: Calling memfd_create with these flags on an older kernel will result in EINVAL because the kernel treats them as unknown bits in the flags argument [4][6]. Starting with Linux 6.3, the kernel also introduced a pid-namespaced sysctl, vm.memfd_noexec, which can influence how memfd_create behaves if these new flags are not provided [7][3]. On systems with this sysctl enabled (e.g., set to 2), calling memfd_create without MFD_EXEC or MFD_NOEXEC_SEAL may be rejected [7][3]. For compatibility with older kernels, applications must either perform a runtime check for the kernel version or use a compatibility header that defines these constants if they are missing [8][9]. Codebases that adopted these flags often implement a fallback or use conditional compilation to ensure they do not pass these flags to memfd_create on systems where they are unsupported [8].
Citations:
- 1: https://git.b0tt0m.xyz/mirror/linux/commit/6fd7353829cafc4067aad9eea0dc95da67e7df16
- 2: https://lwn.net/Articles/890096/
- 3: https://www.phoronix.com/news/Linux-6.3-MM
- 4: https://man7.org/linux/man-pages/man2/memfd_create.2.html
- 5: https://www.man7.org/linux/man-pages/man2/f_add_seals.2const.html
- 6: https://man.archlinux.org/man/memfd_create.2.en
- 7: https://docs.kernel.org/userspace-api/mfd_noexec.html
- 8: memfd_create() without MFD_EXEC nor MFD_NOEXEC_SEAL lxc/lxc#4315
- 9: php-fpm @boot: "memfd_create() without MFD_EXEC nor MFD_NOEXEC_SEAL" php/php-src#13020
🌐 Web query:
Python 3.12 fcntl F_SEAL_EXEC constant documented
💡 Result:
The F_SEAL_EXEC constant is not documented in the Python 3.12 fcntl module documentation [1]. While F_SEAL_EXEC is a valid Linux kernel fcntl constant used for file sealing [2][3][4], it is not explicitly exposed as a member of the Python fcntl module in Python 3.12 [1]. The fcntl module in Python does expose several other F_SEAL_* constants (such as F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_GROW, and F_SEAL_WRITE) [5][6], but F_SEAL_EXEC is absent from the module's implementation and documentation [5][1][6]. If you require this constant in your code, you must define it manually using its known Linux value, which is 0x0020 [2][3][4].
Citations:
- 1: https://docs.python.org/3/library/fcntl.html
- 2: https://github.com/torvalds/linux/blob/master/include/uapi/linux/fcntl.h
- 3: https://codebrowser.dev/linux/include/x86_64-linux-gnu/bits/fcntl-linux.h.html
- 4: https://lkml.indiana.edu/hypermail/linux/kernel/2212.0/07252.html
- 5: https://github.com/python/cpython/blob/e9684fac5a158be9806304a676e619857520a4dc/Modules/fcntlmodule.c
- 6: https://github.com/python/cpython/blob/master/Modules/fcntlmodule.c
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lines around seal application in src/bootstrap_archive.py =="
sed -n '380,435p' src/bootstrap_archive.py | cat -n
echo "== F_SEAL_EXEC references =="
rg -n "F_SEAL_EXEC|F_ADD_SEALS|F_GET_SEALS|required_seals|optional_exec_seal|BootstrapError" src tests || trueRepository: 0cwa/PixeneOS
Length of output: 6535
🌐 Web query:
Python Modules fcntlmodule.c F_SEAL_EXEC F seal execution added 3.12
💡 Result:
The F_SEAL_EXEC constant is not explicitly added to the Python fcntl module as of Python 3.12 [1][2][3]. While Python 3.8 introduced support for various F_SEAL_* constants in the fcntl module to support os.memfd_create [1][2][4], F_SEAL_EXEC was introduced later in the Linux kernel (Linux 6.3) to prevent the modification of executable bits on memfd file descriptors [5][6][7]. Official Python documentation and source code (Modules/fcntlmodule.c) show that the fcntl module exposes constants dynamically based on the features available in the underlying system headers at the time of Python's compilation [8]. If the host system's C library headers include F_SEAL_EXEC (which would require a relatively recent Linux kernel and glibc), it might be available in fcntl as an attribute, but it was not specifically added or documented as a new feature in Python 3.12 [1][2]. The changes in Python 3.12 for the fcntl module were focused on adding FICLONE and FICLONERANGE constants [1][2].
Citations:
- 1: https://github.com/python/cpython/blob/main/Doc/library/fcntl.rst
- 2: https://docs.python.org/3/library/fcntl.html
- 3: https://pradyunsg-cpython-lutra-testing.readthedocs.io/en/latest/library/fcntl.html
- 4: aeros/cpython@8cbb5b6
- 5: https://github.com/torvalds/linux/blob/ea1013c1539270e372fc99854bc6e4d94eaeff66/include/uapi/linux/fcntl.h
- 6: https://sources.debian.org/src/linux/7.1.3-1~bpo13%2B1/Documentation/userspace-api/mfd_noexec.rst
- 7: https://docs.rs/pentacle/latest/pentacle/struct.SealOptions.html
- 8: https://github.com/python/cpython/blob/master/Modules/fcntlmodule.c
🌐 Web query:
git commit python fcntlmodule.c F_SEAL_EXEC
💡 Result:
The constant F_SEAL_EXEC, introduced in Linux 6.3 [1], is a file sealing flag used to prevent the modification of execute bits (chmod) on a memory-backed file created with memfd_create [2][3]. As of August 2026, F_SEAL_EXEC is not explicitly listed among the constants exposed by the Python fcntl module in the official CPython source code (Modules/fcntlmodule.c) [4][5]. While the fcntl module exposes various other sealing constants such as F_ADD_SEALS, F_GET_SEALS, F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_GROW, F_SEAL_WRITE, and F_SEAL_FUTURE_WRITE [4][6][7], F_SEAL_EXEC has not yet been added to the module's macro definitions [4][5]. Consequently, it is not documented in the standard Python library documentation for the fcntl module [7].
Citations:
- 1: tuliom/glibc@42c960a
- 2: https://lkml.iu.edu/2212.0/07252.html
- 3: https://git.zx2c4.com/linux-rng/commit/mm/memfd.c?h=jd%2Fvdso-test-harness&id=6fd7353829cafc4067aad9eea0dc95da67e7df16
- 4: https://github.com/python/cpython/blob/master/Modules/fcntlmodule.c
- 5: https://github.com/python/cpython/blob/main/Modules/fcntlmodule.c
- 6: https://github.com/python/cpython/blob/main/Doc/library/fcntl.rst
- 7: https://docs.python.org/3/library/fcntl.html
🌐 Web query:
Linux 6.3 memfd F_SEAL_EXEC added include uapi linux fcntl.h F_SEAL_EXEC
💡 Result:
The F_SEAL_EXEC seal was introduced in Linux 6.3 as part of a patch series aimed at providing finer control over the execute bits of memfd-created files [1][2]. This seal is defined in the uapi header file include/uapi/linux/fcntl.h [3][4]. When applied, F_SEAL_EXEC prevents any further modifications to the execute bits (the 0111 octal mask or S_IXUSR | S_IXGRP | S_IXOTH) of the memfd file mode using chmod(2) or similar calls [5][4]. Any attempt to modify these bits after the seal is applied will result in an EPERM error [5][6]. This feature helps secure systems by allowing processes to lock a memfd's execution state, preventing attackers from modifying a non-executable memfd into an executable one (a common NoExec bypass technique) [1][2]. In the Linux kernel source, F_SEAL_EXEC is included in the include/uapi/linux/fcntl.h header: #define F_SEAL_EXEC 0x0020 /* prevent chmod modifying exec bits */ This seal is part of the broader effort to support non-executable memfds, often used in conjunction with the MFD_NOEXEC_SEAL flag for memfd_create to ensure a file is created without execute permissions and remains sealed against future modifications [7][8].
Citations:
- 1: https://lwn.net/Articles/890096/
- 2: https://www.phoronix.com/news/Linux-6.3-MM
- 3: https://github.com/torvalds/linux/blob/master/include/uapi/linux/fcntl.h
- 4: https://lkml.iu.edu/2212.0/07252.html
- 5: https://git.b0tt0m.xyz/mirror/linux/commit/6fd7353829cafc4067aad9eea0dc95da67e7df16
- 6: https://lkml.iu.edu/hypermail/linux/kernel/2212.1/01872.html
- 7: https://docs.kernel.org/userspace-api/mfd_noexec.html
- 8: https://docs.kernel.org/7.0/userspace-api/mfd_noexec.html
🌐 Web query:
CPython fcntlmodule.c F_SEAL_EXEC fcntl-seal-exec execution F_SEAL_EXEC
💡 Result:
F_SEAL_EXEC is a Linux kernel feature introduced in version 6.3 [1][2] that, when applied to a file descriptor using the fcntl(fd, F_ADD_SEALS, F_SEAL_EXEC) system call, prevents the modification of the execution permission bits (e.g., via chmod) for that specific file [3][2][4]. In the context of CPython, the fcntl module provides access to this constant [5][6]. While the CPython source file Modules/fcntlmodule.c exposes various F_SEAL_* constants (such as F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_GROW, and F_SEAL_WRITE) [7][5], the availability of F_SEAL_EXEC in your Python environment depends on the underlying system's headers (specifically, if the Linux kernel headers or glibc headers present at build time included it) [1][8]. Summary of Key Points: - Purpose: It prevents chmod from modifying the execute bits (the 0111 bits) of an inode [2]. - Kernel Origin: Added in Linux kernel 6.3 [1][2]. - Usage: It is used with F_ADD_SEALS and F_GET_SEALS, typically in conjunction with memfd_create(2) [9][4]. - CPython Status: CPython's fcntl module adds these constants to the module namespace if they are available in the system's fcntl.h header during the build process [7][5]. You can check if it is available in your environment by running print(fcntl.F_SEAL_EXEC) in Python; if it raises an AttributeError, your installed version of Python was likely built on a system where this constant was not yet defined in the kernel headers [6].
Citations:
- 1: http://public-inbox.org/libc-alpha/20230824203625.1343061-1-kolyshkin@gmail.com/
- 2: https://lkml.indiana.edu/hypermail/linux/kernel/2212.0/07252.html
- 3: https://github.com/torvalds/linux/blob/111e7b23/include/uapi/linux/fcntl.h
- 4: https://man7.org/linux/man-pages/man2/f_get_seals.2const.html
- 5: https://github.com/python/cpython/blob/main/Modules/fcntlmodule.c
- 6: https://github.com/python/cpython/blob/main/Doc/library/fcntl.rst
- 7: https://github.com/python/cpython/blob/master/Modules/fcntlmodule.c
- 8: https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/bits/fcntl-linux.h.html
- 9: https://shadow.github.io/docs/rust/linux_api/fcntl/enum.FcntlSealType.html
🌐 Web query:
"commit" "fcntlmodule.c" "F_SEAL_EXEC" "Python"
💡 Result:
The Python fcntl module currently does not expose F_SEAL_EXEC as a constant. While the Linux kernel supports F_SEAL_EXEC to prevent memfd file descriptors from being made executable [1], the official Python CPython implementation in Modules/fcntlmodule.c only includes a specific subset of sealing constants—such as F_ADD_SEALS, F_GET_SEALS, F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_GROW, F_SEAL_WRITE, and F_SEAL_FUTURE_WRITE—depending on the underlying system support [2][3][4][5]. Because F_SEAL_EXEC is not explicitly present in the fcntlmodule.c constant registration logic, it is unavailable as a named attribute in the fcntl module [2][3]. Users requiring this functionality would currently need to define the integer value manually or use a different mechanism if support has not been upstreamed to the Python standard library [4][5].
Citations:
- 1: https://sources.debian.org/src/linux/7.2~rc3-1~exp1/Documentation/userspace-api/mfd_noexec.rst
- 2: https://github.com/python/cpython/blob/master/Modules/fcntlmodule.c
- 3: https://github.com/python/cpython/blob/main/Modules/fcntlmodule.c
- 4: https://github.com/python/cpython/blob/main/Doc/library/fcntl.rst
- 5: https://docs.python.org/3/library/fcntl.html
🌐 Web query:
site:github.com/python/cpython "F_SEAL_EXEC" "Modules/fcntlmodule.c"
💡 Result:
There is no F_SEAL_EXEC constant associated with file sealing in Linux or the Python fcntl module. The confusion likely stems from a misunderstanding of the Linux file sealing API, which is implemented in Python's Modules/fcntlmodule.c [1][2]. Linux provides several constants for sealing file descriptors created with memfd_create [3], including: F_SEAL_SEAL: Prevents further modifications to the set of seals [3]. F_SEAL_SHRINK: Prevents the file from being shrunk [3]. F_SEAL_GROW: Prevents the file from being grown [3]. F_SEAL_WRITE: Prevents any write access to the file [3]. F_SEAL_FUTURE_WRITE: Prevents future writes to the file, while allowing existing shared writable mappings (available on Linux >= 5.1) [4][5]. Modules/fcntlmodule.c exposes these constants to Python by mapping them to their corresponding Linux kernel definitions [1][2]. There is no constant named F_SEAL_EXEC in the Linux kernel's fcntl.h or the Python fcntl module [1][3].
Citations:
- 1: https://github.com/python/cpython/blob/main/Modules/fcntlmodule.c
- 2: https://github.com/python/cpython/blob/master/Modules/fcntlmodule.c
- 3: Add file-sealing ops to fcntl python/cpython#71022
- 4: https://github.com/python/cpython/blob/main/Doc/library/fcntl.rst
- 5: https://github.com/python/cpython/blob/master/Doc/library/fcntl.rst
🌐 Web query:
https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/fcntl.h F_SEAL_EXEC
💡 Result:
F_SEAL_EXEC is a file seal in the Linux kernel, defined in include/uapi/linux/fcntl.h, used to prevent the modification of a file's executable bits [1][2]. When F_SEAL_EXEC is applied to a file (typically a memory-backed file created via memfd_create), it prevents any subsequent chmod(2) or similar operations from altering the executable mode bits (0111) of the file [2][3]. Any attempt to modify these bits after the seal is applied will result in an EPERM error [2][3]. This ensures that the file's executable status, whether executable or non-executable, is permanently locked [3]. F_SEAL_EXEC is primarily used in conjunction with the memfd_create(2) system call [4][5]. It is notably employed by the MFD_NOEXEC_SEAL flag, which creates a non-executable memory file and automatically applies the F_SEAL_EXEC seal to ensure it cannot be changed to executable later [4][6]. This mechanism is part of a broader effort to harden systems against the execution of unverified code from memory-resident files, helping align memfd usage with security policies that might otherwise restrict executable memory [7][5]. In summary: - Purpose: Prevents modification of execute permission bits (chmod) [2][3]. - Usage: Applied via fcntl(fd, F_ADD_SEALS, F_SEAL_EXEC) or indirectly via MFD_NOEXEC_SEAL during memfd_create [4][2]. - Enforcement: Kernel rejects chmod attempts modifying the 0111 bits with EPERM [2][3].
Citations:
- 1: https://github.com/torvalds/linux/blob/ea1013c1539270e372fc99854bc6e4d94eaeff66/include/uapi/linux/fcntl.h
- 2: https://lkml.iu.edu/2212.0/07252.html
- 3: https://git.b0tt0m.xyz/mirror/linux/commit/6fd7353829cafc4067aad9eea0dc95da67e7df16
- 4: https://docs.kernel.org/userspace-api/mfd_noexec.html
- 5: https://lwn.net/Articles/890096/
- 6: https://gbmc.googlesource.com/linux/+/34e1a5d43c5deec563b94f3330b690dde9d1de53/Documentation/userspace-api/mfd_noexec.rst
- 7: https://lwn.net/Articles/977669/
🌐 Web query:
"commit" "F_SEAL_EXEC" "fcntl" "Python"
💡 Result:
There is no F_SEAL_EXEC constant in the Python fcntl module or the Linux kernel's file sealing API [1][2]. File sealing, which is used in conjunction with memfd_create to restrict operations on a file descriptor, includes several standard constants in the Python fcntl module [1][3]. These were introduced in Python 3.8 to support Linux-specific file sealing operations [1][4]. The currently available sealing constants in Python's fcntl module include [1][5]: - F_ADD_SEALS: Used to add seals to a file descriptor [1][2]. - F_GET_SEALS: Used to retrieve the current seals on a file descriptor [1][2]. - F_SEAL_SEAL: Prevents further seals from being added [5][2]. - F_SEAL_SHRINK: Prevents the file from being shrunk [5][2]. - F_SEAL_GROW: Prevents the file from being grown [5][2]. - F_SEAL_WRITE: Prevents modifications to the file contents [5][2]. - F_SEAL_FUTURE_WRITE: Available on Linux >= 5.1, prevents future writes to the file [1]. If you are looking for a way to restrict execution of a file, file sealing is not the mechanism used for that purpose [2]. Sealing is specifically designed for managing file content modifications and size changes for memory-backed files [2].
Citations:
- 1: https://docs.python.org/3/library/fcntl.html
- 2: https://man7.org/linux/man-pages/man2/f_get_seals.2const.html
- 3: https://docs.python.org/3.11/library/fcntl.html
- 4: https://bugs.python.org/issue26835
- 5: https://github.com/python/cpython/blob/e9684fac5a158be9806304a676e619857520a4dc/Modules/fcntlmodule.c
🌐 Web query:
Python fcntl F_SEAL_EXEC documentation 3.13 3.14
💡 Result:
As of August 8, 2026, the Python fcntl module does not explicitly expose the F_SEAL_EXEC constant in its official documentation for versions 3.13 or 3.14 [1][2]. F_SEAL_EXEC is a Linux-specific constant introduced in Linux kernel 6.3 [3][4] to prevent chmod operations from modifying the execute bits of a memory-backed file descriptor (memfd) [5][6][4]. While the Python fcntl module supports various sealing constants (F_ADD_SEALS, F_GET_SEALS, and other F_SEAL_* constants) [1][2], F_SEAL_EXEC is currently omitted from the module's public constant set in the Python standard library. If you require this functionality in a Python application, you can define the constant manually using its integer value, which is 0x0020 (32) [5][7][8], and pass it to fcntl.fcntl using the F_ADD_SEALS command. Example usage: import fcntl F_SEAL_EXEC = 0x0020 # Assuming fd is a memfd file descriptor fcntl.fcntl(fd, fcntl.F_ADD_SEALS, F_SEAL_EXEC) Because the Python standard library has not yet added this specific constant, it will not be available in the fcntl namespace by default. Users of Linux 6.3 or newer kernels can safely use the manual definition provided above to interact with this kernel feature [3][4].
Citations:
- 1: https://docs.python.org/3/library/fcntl.html
- 2: https://github.com/python/cpython/blob/master/Doc/library/fcntl.rst
- 3: https://public-inbox.org/libc-alpha/12280820.O9o76ZdvQC@pwmachine/T/
- 4: https://lkml.indiana.edu/hypermail/linux/kernel/2212.0/07252.html
- 5: https://github.com/torvalds/linux/blob/master/include/uapi/linux/fcntl.h
- 6: https://docs.kernel.org/userspace-api/mfd_noexec.html
- 7: https://shadow.github.io/docs/rust/linux_api/fcntl/enum.FcntlSealType.html
- 8: https://docs.rs/lolc/latest/lolc/constant.F_SEAL_EXEC.html
🌐 Web query:
site:github.com/mirror python repo python search "F_SEAL_EXEC"
💡 Result:
There is no standard Linux kernel constant or fcntl flag named F_SEAL_EXEC. The search results for F_SEAL_EXEC within various open-source repositories (such as QEMU, BusyBox, Make, and others) do not show this constant being used as a file-sealing flag. Instead, the results highlight common Linux system programming concepts, such as: 1. File Sealing: Linux supports file sealing for memory-backed files (created via memfd_create) using constants like F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_GROW, and F_SEAL_WRITE. 2. Execution Control: The search results frequently reference FD_CLOEXEC, which is a standard fcntl flag used to ensure that a file descriptor is automatically closed when an execve system call is executed. 3. No-Exec/No-Fork: In projects like BusyBox, there are conceptual discussions regarding NOEXEC and NOFORK optimizations, which are methods for executing applets without performing a full fork/exec sequence, but these are not related to a specific fcntl seal constant. If you encountered F_SEAL_EXEC, it may be a custom internal constant within a specific, non-mainline codebase or a misunderstanding of existing file sealing or execution-related flags. Based on available authoritative sources, F_SEAL_EXEC is not part of the standard Linux API.
Citations:
- 1: https://github.com/mirror/busybox/blob/371fe9f7/NOFORK_NOEXEC.lst
- 2: https://github.com/mirror/busybox/blob/24198f652f10dca5603df7c704263358ca21f5ce/docs/nofork_noexec.txt
- 3: https://github.com/mirror/busybox/blob/master/util-linux/fsfreeze.c
- 4: https://github.com/mirror/make/blob/master/src/posixos.c
- 5: https://github.com/mirror/smartmontools/blob/master/os_linux.cpp
🌐 Web query:
"define" "F_SEAL_EXEC" "fcntl.h" lwn F_SEAL_EXEC memfd chmod
💡 Result:
F_SEAL_EXEC is a Linux kernel seal flag (defined in include/uapi/linux/fcntl.h) used with the memfd_create system call and the fcntl F_ADD_SEALS operation [1][2]. Its primary purpose is to prevent the modification of a file's executable bits [2][3]. When F_SEAL_EXEC is applied to a memfd, any subsequent attempt to modify the file's executable status via chmod or similar operations that alter the mode's execute bits (the 0111 octal mask) will be blocked, returning an EPERM error [2][4][3]. Key points regarding F_SEAL_EXEC: 1. Purpose: It preserves the executable state of a memfd (making it either permanently executable or permanently non-executable) once the seal is applied [3]. 2. Interaction with Chmod: The seal specifically protects against modifications to the file mode's execute bits [2][3]. If a chmod call attempts to change these bits on a sealed memfd, the kernel denies the request [2][3]. 3. Relationship with memfd_create: F_SEAL_EXEC is often used in conjunction with the MFD_NOEXEC_SEAL flag [5]. MFD_NOEXEC_SEAL is a convenient combination of MFD_NOEXEC, MFD_ALLOW_SEALING, and F_SEAL_EXEC, which is designed to promote secure memfd usage by creating a non-executable memfd that cannot be made executable later [5][6]. 4. Implementation Detail: When F_SEAL_EXEC is added, if the file is currently executable, the kernel may automatically apply additional seals (such as F_SEAL_WRITE, F_SEAL_SHRINK, etc.) to enforce W^X (Write XOR Execute) memory protection policies [7]. This flag was introduced to help mitigate security risks where malicious code might be executed from a memfd that was originally created without restrictive permissions [8].
Citations:
- 1: https://github.com/torvalds/linux/blob/master/include/uapi/linux/fcntl.h
- 2: https://lkml.indiana.edu/hypermail/linux/kernel/2212.0/07252.html
- 3: https://git.b0tt0m.xyz/mirror/linux/commit/6fd7353829cafc4067aad9eea0dc95da67e7df16
- 4: https://lkml.iu.edu/2212.0/05745.html
- 5: https://docs.kernel.org/userspace-api/mfd_noexec.html
- 6: https://lwn.net/Articles/977669/
- 7: https://github.com/torvalds/linux/blob/master/mm/memfd.c
- 8: https://lwn.net/Articles/890096/
🌐 Web query:
"gitlab.com" "python" "fcntl" "F_SEAL_EXEC"
💡 Result:
F_SEAL_EXEC is a Linux kernel constant introduced in kernel version 6.3 [1] that prevents the modification of execution bits (specifically the chmod-based changes to the executable bits) for memfd-created file descriptors [2][3]. Regarding the Python fcntl module: The Python fcntl module exposes constants for file sealing, but as of the current Python documentation (up to version 3.14), F_SEAL_EXEC is not explicitly listed as a standard constant exposed by the fcntl module [4][5][6]. The fcntl module's support for sealing (introduced in Python 3.8) typically includes F_ADD_SEALS, F_GET_SEALS, and the core seals like F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_GROW, and F_SEAL_WRITE [4][7][8]. Because F_SEAL_EXEC is a relatively newer addition to the Linux UAPI headers [1], it may not be automatically available in the Python fcntl module if the Python binary was compiled against older kernel headers, or if the module simply does not yet include this specific constant in its interface [7]. If the constant is missing from the fcntl module, you can define it manually in your Python code using its integer value, which is 0x0020 [9][2][3]. Example of manual definition: import fcntl F_SEAL_EXEC = 0x0020 # Now use F_SEAL_EXEC with fcntl.fcntl(fd, fcntl.F_ADD_SEALS,...) GitLab projects (such as kernel-ark or glibc-related repositories) often reference this constant within the context of kernel development or package builds [10][11], but it remains a low-level Linux system call feature rather than a high-level Python abstraction.
Citations:
- 1: https://sourceware.org/pipermail/glibc-cvs/2023q3/082848.html
- 2: https://git.b0tt0m.xyz/mirror/linux/commit/6fd7353829cafc4067aad9eea0dc95da67e7df16
- 3: https://lkml.iu.edu/2212.0/05779.html
- 4: https://docs.python.org/3/library/fcntl.html
- 5: https://docs.python.org/3.11/library/fcntl.html
- 6: https://docs.python.org/3.10/library/fcntl.html
- 7: https://github.com/python/cpython/blob/e9684fac5a158be9806304a676e619857520a4dc/Modules/fcntlmodule.c
- 8: https://github.com/python/cpython/blob/3.11/Doc/library/fcntl.rst
- 9: https://github.com/torvalds/linux/blob/111e7b23/include/uapi/linux/fcntl.h
- 10: https://gitlab.com/cki-project/kernel-ark/-/tree/kernel-6.3.0-0.rc0.f3a2439f20d9.9/fs/erofs?ref_type=tags
- 11: https://gitlab.com/redhat/centos-stream/rpms/glibc/-/blob/c10s/glibc.spec
🌐 Web query:
https://news.ycombinator.com/item?id=35351539 F_SEAL_EXEC memfd
💡 Result:
F_SEAL_EXEC is a sealing flag for Linux memfd files that prevents modification of the file's executable bits [1][2]. When applied to a memfd, this seal ensures that the file's execute permissions cannot be changed (e.g., via chmod) after creation [3][4]. Key details regarding F_SEAL_EXEC and related memfd security features include: Purpose and Security: Historically, all memfds were created with execute permissions enabled by default [5][2]. This created a security risk where an attacker could use an anonymous, in-memory file to execute malicious code, potentially bypassing security policies that restrict code execution [2][4]. F_SEAL_EXEC was introduced to help mitigate this risk by allowing processes to seal a file as non-executable [2][4]. W^X Enforcement: When F_SEAL_EXEC is set on an executable memfd, the kernel automatically applies additional write seals (including F_SEAL_WRITE, F_SEAL_SHRINK, and F_SEAL_GROW) to enforce a "Write XOR Execute" (W^X) policy [1][6]. This prevents the file from being both writable and executable, protecting against scenarios where code could be modified after being marked as executable [6]. Creation Flags: MFD_NOEXEC_SEAL: When this flag is used with memfd_create, the resulting memfd is created without executable bits and is automatically sealed with F_SEAL_EXEC [5][3]. This effectively renders the memfd non-executable and prevents it from ever being marked as executable [4]. MFD_EXEC: This flag explicitly creates a memfd with executable permissions enabled, maintaining the traditional default behavior of memfd_create [5][4]. Sysctl Control: The kernel includes a PID-namespaced sysctl, vm.memfd_noexec, which allows administrators to control the default behavior of memfd_create when neither MFD_EXEC nor MFD_NOEXEC_SEAL is specified [5][7]. This setting can be used to enforce non-executable memfds system-wide or within specific containers, helping to improve security by default without breaking legacy applications that expect the old behavior [5][4]. In summary, F_SEAL_EXEC is a core component of the modern memfd security API, designed to lock down the execute permissions of in-memory files and support W^X memory protection policies [1][2][4].
Citations:
- 1: https://github.com/torvalds/linux/blob/master/mm/memfd.c
- 2: https://lwn.net/Articles/890096/
- 3: https://lists.openwall.net/linux-kernel/2022/12/06/826
- 4: https://lwn.net/Articles/918106/
- 5: https://docs.kernel.org/userspace-api/mfd_noexec.html
- 6: https://lists.openwall.net/linux-kernel/2026/05/11/999
- 7: https://gbmc.googlesource.com/linux/+/34e1a5d43c5deec563b94f3330b690dde9d1de53/Documentation/userspace-api/mfd_noexec.rst
🌐 Web query:
"Pull #10455" "F_SEAL_EXEC" "fcntlmodule.c"
💡 Result:
The reference to Pull #10455 regarding F_SEAL_EXEC and fcntlmodule.c does not appear to correspond to the primary development history of these constants in the CPython repository. The introduction of file-sealing operations into the Python fcntl module was primarily handled via bpo-26835, which resulted in pull requests such as #13015 [1] and its successor #13694 [1][2]. These changes added constants like F_ADD_SEALS, F_GET_SEALS, and F_SEAL_* to Modules/fcntlmodule.c [2][3], which were subsequently documented in Python 3.8 [4][5]. While F_SEAL_EXEC is a valid constant defined in the Linux kernel headers (include/uapi/linux/fcntl.h) [6], it is not explicitly listed among the constants added to the CPython fcntl module in the initial file-sealing patch [2]. The fcntl module dynamically exposes constants available on the host system at compile time, meaning that if a system's header files define F_SEAL_EXEC, it may be available through the fcntl module, but it was not the subject of the specific pull request commonly associated with adding basic file sealing support to CPython [1][2].
Citations:
- 1: bpo-26835: Add file-sealing ops to fcntl python/cpython#13015
- 2: aeros/cpython@8cbb5b6
- 3: https://github.com/python/cpython/blob/e9684fac5a158be9806304a676e619857520a4dc/Modules/fcntlmodule.c
- 4: https://docs.python.org/3.11/library/fcntl.html
- 5: https://github.com/python/cpython/blob/3.11/Doc/library/fcntl.rst
- 6: https://github.com/torvalds/linux/blob/111e7b23/include/uapi/linux/fcntl.h
🌐 Web query:
"python" "commit" "F_SEAL_EXEC" "fcntlmodule.c" "10455"
💡 Result:
It looks like your query is asking for a specific Python commit ID plus a code location, but only provides: "python commit F_SEAL_EXEC fcntlmodule.c 10455". Based on authoritative sources, F_SEAL_EXEC is a Linux memfd/file-sealing constant (prevents chmod from modifying the executable bits) that is defined in Linux as F_SEAL_EXEC=0x0020 [1][2][3]. Python exposes sealing constants in its fcntl module documentation (F_ADD_SEALS, F_GET_SEALS, and F_SEAL_* constants) [4]. The relevant Python implementation for fcntl is in Modules/fcntlmodule.c [5]. However, I cannot validate a specific “python commit … 10455” (there’s no clear mapping in the sources above between the number 10455 and a particular CPython commit that adds/changes F_SEAL_EXEC in fcntlmodule.c). To finish this precisely, I’d need one of: 1) the actual commit hash (full/short) you mean, or 2) the repository/branch you’re referring to (cpython vs another fork), or 3) what “10455” corresponds to (e.g., PR number, line number, or an internal ID).
Citations:
- 1: https://public-inbox.org/libc-alpha/12280820.O9o76ZdvQC@pwmachine/T/
- 2: https://git.b0tt0m.xyz/mirror/linux/commit/6fd7353829cafc4067aad9eea0dc95da67e7df16
- 3: https://lkml.indiana.edu/hypermail/linux/kernel/2212.1/01872.html
- 4: https://docs.python.org/3/library/fcntl.html
- 5: https://github.com/python/cpython/blob/main/Modules/fcntlmodule.c
🌐 Web query:
"6fd7353829cafc4067aad9eea0dc95da67e1e5" "F_SEAL_EXEC" "fcntl.h"
💡 Result:
F_SEAL_EXEC is a Linux kernel feature defined in the header file include/uapi/linux/fcntl.h [1][2]. It is a file seal used with memory-backed files (memfd) to provide security by preventing the modification of a file's executable bits [3]. When the F_SEAL_EXEC seal is applied to a file, any subsequent attempt to modify the executable bits (the 'x' bits) of the file mode using chmod will fail with an EPERM error [3]. This mechanism is part of the broader memfd sealing framework, which allows applications to create memory-backed files that are non-executable and protected from future modifications [4][3]. The string "6fd7353829cafc4067aad9eea0dc95da67e1e5" does not appear to be a recognized technical identifier, commit hash, or standard value associated with F_SEAL_EXEC or the Linux kernel source tree; it appears to be unrelated to the technical context of the query [1-5]. Key details regarding F_SEAL_EXEC: - Header file: include/uapi/linux/fcntl.h [1][2] - Definition: #define F_SEAL_EXEC 0x0020 [1][2] - Purpose: Prevents chmod from modifying the executable bits of the file [3]. - Use case: Frequently used in conjunction with memfd_create, specifically with the MFD_NOEXEC_SEAL flag, to create secure, non-executable memory mappings that cannot be subsequently made executable [4].
Citations:
- 1: https://github.com/torvalds/linux/blob/master/include/uapi/linux/fcntl.h
- 2: https://codebrowser.dev/linux/linux/include/uapi/linux/fcntl.h.html
- 3: https://lkml.indiana.edu/hypermail/linux/kernel/2212.1/06259.html
- 4: https://docs.kernel.org/userspace-api/mfd_noexec.html
Make F_SEAL_EXEC optional before using sealing with sealed execution.
MFD_NOEXEC_SEAL cannot be used here because sealing must still permit execute bits; the alternate path is applying F_SEAL_EXEC. This seal requires Linux 6.3+. On kernels older than that, fcntl(..., F_ADD_SEALS, ... F_SEAL_EXEC ...) can fail and the current except OSError converts it into BootstrapError, so sealed execution stops on older Linux systems. Apply the required seals first, then attempt F_SEAL_EXEC separately and ignore unavailable-kernel failures.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/bootstrap_archive.py` around lines 406 - 417, The sealing flow around
required_seals and requested_seals must treat F_SEAL_EXEC as optional for older
kernels. Apply required_seals first, then separately attempt F_SEAL_EXEC when
available, ignoring the unavailable-kernel OSError while preserving other
failures; validate applied_seals against the required seals and include
F_SEAL_EXEC only when successfully applied.
Source: Linters/SAST tools
| yield descriptor | ||
| except OSError: | ||
| _fail("sealed executable preparation failed") |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
Move yield out of the except OSError scope.
yield descriptor runs inside the try block that line 430 guards. If the consumer body raises OSError, the generator receives it at the yield point, and this handler reports "sealed executable preparation failed". run in src/bootstrap_executable_tools.py calls os.execve inside this context. An execve failure such as ENOEXEC or EACCES is therefore reported as a preparation failure, and the errno is lost.
The path still fails closed. Only the failure attribution is wrong.
♻️ Proposed refactor to scope the handler to preparation only
_fail("sealed executable does not match the locked bytes and mode")
- yield descriptor
except OSError:
_fail("sealed executable preparation failed")
- finally:
+ try:
+ yield descriptor
+ finally:
os.close(descriptor)This requires restructuring so that descriptor is closed on both the preparation-failure path and the consumer path. An ExitStack or a nested helper keeps that explicit.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/bootstrap_archive.py` around lines 429 - 431, Restructure the generator
around the preparation logic so the OSError handler covers only descriptor
preparation, not the subsequent yield. In the function containing yield
descriptor, ensure descriptor is closed on both preparation failure and
consumer/execve failure paths, using the existing cleanup pattern or an
ExitStack, while preserving the sealed executable failure behavior and
propagating consumer OSError with its original errno.
| def sanitized_environment(source: Mapping[str, str]) -> dict[str, str]: | ||
| result: dict[str, str] = {} | ||
| for name, value in source.items(): | ||
| if not isinstance(name, str) or not isinstance(value, str): | ||
| fail("executable environment must contain only text") | ||
| if name in UNSAFE_ENVIRONMENT_NAMES or name.startswith( | ||
| UNSAFE_ENVIRONMENT_PREFIXES | ||
| ): | ||
| continue | ||
| if not name or "=" in name or "\x00" in name or "\x00" in value: | ||
| fail("executable environment contains an unsafe entry") | ||
| result[name] = value | ||
| return result |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Replace the environment denylist with an allowlist.
sanitized_environment removes LD_*, DYLD_*, and five named variables. glibc honours further variables that cause the process to load attacker-chosen files. GCONV_PATH and LOCPATH are the notable cases: neither starts with LD_, and GCONV_PATH makes glibc load shared objects from a caller-supplied directory. Other loaders add more, for example PERL5LIB, PYTHONPATH, and NODE_OPTIONS.
A denylist must be updated for every new variable. An allowlist fails closed. The locked tools are avbroot, afsr, and custota-tool, so the required set is small.
🛡️ Proposed fix to allowlist the executable environment
-UNSAFE_ENVIRONMENT_NAMES = frozenset(
- ("BASH_ENV", "ENV", "GLIBC_TUNABLES", "RUST_BACKTRACE", "RUST_LOG")
-)
-UNSAFE_ENVIRONMENT_PREFIXES = ("LD_", "DYLD_")
+ALLOWED_ENVIRONMENT_NAMES = frozenset(("HOME", "LANG", "PATH", "TERM", "TMPDIR")) def sanitized_environment(source: Mapping[str, str]) -> dict[str, str]:
result: dict[str, str] = {}
for name, value in source.items():
if not isinstance(name, str) or not isinstance(value, str):
fail("executable environment must contain only text")
- if name in UNSAFE_ENVIRONMENT_NAMES or name.startswith(
- UNSAFE_ENVIRONMENT_PREFIXES
- ):
+ if name not in ALLOWED_ENVIRONMENT_NAMES:
continue
if not name or "=" in name or "\x00" in name or "\x00" in value:
fail("executable environment contains an unsafe entry")
result[name] = value
return resultConfirm the exact set that avbroot, afsr, and custota-tool require before you narrow it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/bootstrap_executable_tools.py` around lines 67 - 79, Replace the denylist
logic in sanitized_environment with a confirmed allowlist containing only the
environment variables required by avbroot, afsr, and custota-tool. Validate the
exact required set before narrowing the output, preserve the existing type and
unsafe-entry validation, and exclude all unapproved variables by default.
| receipt = self._receipt(tool) | ||
| if receipt is not None: | ||
| archive_path = self._object_path("archives", receipt["archive_sha256"]) | ||
| signature_path = self._object_path( | ||
| "signatures", receipt["signature_sha256"] | ||
| ) | ||
| self._verify_pair(tool, archive_path, signature_path, trust_path) | ||
| return archive_path, signature_path, receipt["signature_sha256"] |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
A receipt without its cached objects permanently blocks the bootstrap.
_receipt returns a receipt whenever receipts/<sha256>.json exists and is canonical. _verify_pair then opens objects/archives/<digest> and objects/signatures/<digest>. If either object is absent, open_regular calls _fail with "cannot stat executable archive", and install aborts. The download path at lines 219-243 is never reached.
That state is reachable without an attacker. A CI cache restore can keep the small receipt directory and drop the large object directory. An interrupted run can leave the same asymmetry, because atomic_write of the receipt is not ordered against object publication in a crash-safe way.
Re-downloading is not a weaker path. Lines 221-227 apply the same size, digest, signature, and inspection checks. Treat a missing object as a cache miss, and keep fail-closed behaviour for an object that exists but does not verify.
If you accept this change, update docs/executable-tool-trust.md lines 132-133, which currently state that a missing cache entry fails closed.
🛠️ Proposed fix to treat an absent object as a cache miss
receipt = self._receipt(tool)
if receipt is not None:
archive_path = self._object_path("archives", receipt["archive_sha256"])
signature_path = self._object_path(
"signatures", receipt["signature_sha256"]
)
- self._verify_pair(tool, archive_path, signature_path, trust_path)
- return archive_path, signature_path, receipt["signature_sha256"]
+ if archive_path.exists() and signature_path.exists():
+ self._verify_pair(tool, archive_path, signature_path, trust_path)
+ return archive_path, signature_path, receipt["signature_sha256"]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| receipt = self._receipt(tool) | |
| if receipt is not None: | |
| archive_path = self._object_path("archives", receipt["archive_sha256"]) | |
| signature_path = self._object_path( | |
| "signatures", receipt["signature_sha256"] | |
| ) | |
| self._verify_pair(tool, archive_path, signature_path, trust_path) | |
| return archive_path, signature_path, receipt["signature_sha256"] | |
| receipt = self._receipt(tool) | |
| if receipt is not None: | |
| archive_path = self._object_path("archives", receipt["archive_sha256"]) | |
| signature_path = self._object_path( | |
| "signatures", receipt["signature_sha256"] | |
| ) | |
| if archive_path.exists() and signature_path.exists(): | |
| self._verify_pair(tool, archive_path, signature_path, trust_path) | |
| return archive_path, signature_path, receipt["signature_sha256"] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/bootstrap_executable_tools.py` around lines 210 - 217, Update the receipt
fast path in the bootstrap method containing _receipt and _verify_pair so
missing cached archive or signature objects are treated as a cache miss: check
both object paths exist before calling _verify_pair, and fall through to the
existing download and validation flow when either is absent. Continue calling
_verify_pair and failing closed when both objects exist but verification fails,
and update the corresponding cache behavior statement in the executable-tool
trust documentation.
| for tool in tools: | ||
| destination = extracted.get(tool["id"]) | ||
| if destination is None: | ||
| continue | ||
| installed = self._install_path(tool) | ||
| try: | ||
| os.rename(destination, installed) | ||
| fsync_directory(installed.parent) | ||
| except FileExistsError: | ||
| validate_install(installed, tool["layout"]) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
os.rename onto a populated directory does not raise FileExistsError.
Line 311 renames the staged install directory onto installed. If another process created and populated installed after the check at line 291, rename(2) fails with ENOTEMPTY. Python maps EEXIST to FileExistsError, but it maps ENOTEMPTY to plain OSError. The handler at line 313 therefore does not run.
The error escapes to main, which reports "internal operation failed" at line 446. The run fails closed, but two concurrent bootstraps that share one --workdir cannot recover, and the message hides the cause.
🛠️ Proposed fix to revalidate on any rename conflict
try:
os.rename(destination, installed)
fsync_directory(installed.parent)
- except FileExistsError:
+ except (FileExistsError, NotADirectoryError, OSError):
validate_install(installed, tool["layout"])Narrow the handler to errno.EEXIST and errno.ENOTEMPTY so that unrelated OSError values still propagate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/bootstrap_executable_tools.py` around lines 305 - 314, Update the rename
exception handler in the tool-install loop to catch OSError conflicts for both
errno.EEXIST and errno.ENOTEMPTY, while allowing unrelated OSError values to
propagate. Reuse the existing validate_install(installed, tool["layout"])
recovery path for either conflict.
| def ensure_private_directory(path: Path) -> None: | ||
| try: | ||
| path.mkdir(mode=0o700, parents=True, exist_ok=True) | ||
| info = path.lstat() | ||
| except OSError as exc: | ||
| fail(f"cannot create private bootstrap directory: {exc.strerror or exc}") | ||
| if not stat.S_ISDIR(info.st_mode) or stat.S_IMODE(info.st_mode) & 0o077: | ||
| fail("bootstrap directory is not a private real directory") |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Python pathlib Path.mkdir parents=True mode argument ignored for parent directories
💡 Result:
In versions of Python prior to 3.15, the mode argument in pathlib.Path.mkdir is ignored for any parent directories created when parents=True [1][2][3]. In these versions, parent directories are created using default system permissions (modified by the process umask) rather than the mode specified for the final directory [1][4]. This behavior is designed to mimic the standard POSIX mkdir -p command [1][3]. Starting in Python 3.15, a new parent_mode parameter was added to Path.mkdir to address this limitation [5][6]. If parent_mode is provided, it is applied to any newly created intermediate directories, while the mode argument continues to apply to the target directory itself [5][6]. To work around this limitation in older versions of Python, you can manually create the parent directories by iterating through them: for parent in reversed(path.parents): parent.mkdir(mode=0o770, exist_ok=True) path.mkdir(mode=0o770, exist_ok=True) [2]
Citations:
- 1: https://docs.python.org/3/library/pathlib.html
- 2: https://stackoverflow.com/questions/67299076/python-pathlib-path-mkdir-applies-desired-mode-to-final-directory-but-mode
- 3: https://docs.python.org/3.10/library/pathlib.html
- 4: https://docs.python.org/3.11/library/pathlib.html
- 5: https://github.com/python/cpython/blob/main/Doc/library/pathlib.rst
- 6: https://github.com/python/cpython/blob/master/Doc/library/pathlib.rst
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)bootstrap_io\.py$|docs/executable-tool-trust\.md$|^\.python-version$|pyproject\.toml$|requirements|setup\.py$|tox\.ini$' || true
echo "== bootstrap_io.py =="
if [ -f src/bootstrap_io.py ]; then
cat -n src/bootstrap_io.py | sed -n '1,90p'
fi
echo "== relevant docs excerpt =="
if [ -f docs/executable-tool-trust.md ]; then
cat -n docs/executable-tool-trust.md | sed -n '100,145p'
fi
echo "== python version declarations =="
for f in $(git ls-files | rg '(^\.python-version$|pyproject\.toml$|tox\.ini$|setup\.py$|requirements.*\.txt$)'); do
echo "-- $f --"
sed -n '1,120p' "$f" | rg -n 'python|requires-python|target-version|env|py|version' || true
done
echo "== path construction usages =="
rg -n "ensure_private_directory|bootstrap-cache|objects|archives|Path\(" src || true
echo "== local pathlib parents-mode probe (no repo code) =="
python3 - <<'PY'
import pathlib, tempfile, stat, os, shutil
tmp = tempfile.mkdtemp()
try:
root = pathlib.Path(tmp) / "private" / "parent" / "leaf"
root.mkdir(mode=0o700, parents=True, exist_ok=True)
for component in (root.parents, root):
for p in (component if hasattr(component, "__iter__") else []): pass
for p in [root.parent.parent, root.parent, root]:
st = os.lstat(p)
print(f"{p} S_ISDIR={stat.S_ISDIR(st.st_mode)} mode={oct(st.st_mode & 0o777)}")
finally:
shutil.rmtree(tmp, ignore_errors=True)
PYRepository: 0cwa/PixeneOS
Length of output: 8696
🌐 Web query:
Python pathlib pathlib_rst parents parameter mode parameter mkdir does not recurse parent directories in path
💡 Result:
In Python's pathlib module, the Path.mkdir method behaves as follows regarding its parameters: 1. Parents Parameter: When parents=True is set, the method recursively creates any missing parent directories required to build the target path [1][2]. If parents=False (the default), the method will raise a FileNotFoundError if any parent directory in the path is missing [1][3]. 2. Mode Parameter and Recursion: When parents=True is used, intermediate parent directories are created using the default system permissions (subject to the process's umask) [1][2]. The mode parameter provided to mkdir is only applied to the final directory being created, not the parent directories [1][2]. 3. New Parent Mode Parameter: As of Python 3.15, a parent_mode parameter was added to Path.mkdir [2]. This allows you to specify the mode for any newly-created intermediate parent directories when parents=True [2]. If parent_mode is not specified, those intermediate directories continue to be created with default permissions [2]. In summary, for versions of Python prior to 3.15, the mode parameter does not affect parent directories when recursing [1][4]. Starting with Python 3.15, you can use the parent_mode parameter to control the permissions of those automatically created parent directories [2].
Citations:
- 1: https://docs.python.org/3/library/pathlib.html
- 2: https://github.com/python/cpython/blob/main/Doc/library/pathlib.rst
- 3: https://docs.python.org/3.10/library/pathlib.html
- 4: https://docs.python.org/3.11/library/pathlib.html
🌐 Web query:
Python documentation pathlib.Path.mkdir parents mode parent_mode ignored parents directory permissions
💡 Result:
In Python's pathlib module, the behavior of directory permissions when using mkdir(parents=True) depends on your Python version [1][2][3]. Historically, the mode parameter in Path.mkdir applies only to the target directory itself [1][4][5]. When parents=True is specified, any missing intermediate directories are created using default system permissions, ignoring the mode parameter provided to the method [1][6]. This behavior mimics the POSIX mkdir -p command [1][3]. However, starting in Python 3.15, a new keyword-only argument called parent_mode was introduced [2][3]. If you are using Python 3.15 or newer: You can specify parent_mode to control the permissions of intermediate directories created when parents=True [2][3]. Like the standard mode, parent_mode is combined with the process's umask to determine the final access flags [2][3]. If you are using a version older than 3.15: The mode parameter is explicitly ignored for parent directories [1][6]. If you need specific permissions for those parent directories, you must create them separately or use os.chmod to adjust their permissions after they have been created [7].
Citations:
- 1: https://docs.python.org/3/library/pathlib.html
- 2: https://github.com/python/cpython/blob/main/Doc/library/pathlib.rst
- 3: https://github.com/python/cpython/blob/master/Doc/library/pathlib.rst
- 4: https://docs.python.org/3.11/library/pathlib.html
- 5: https://docs.python.org/3.10/library/pathlib.html
- 6: https://github.com/python/cpython/blob/3.10/Doc/library/pathlib.rst
- 7: https://github.com/python/cpython/blob/cd2f234ad2ddd95e2d650edac5a6d593c865756a/Lib/pathlib/__init__.py
🌐 Web query:
Python pathlib.Path.mkdir parents parents does not create parent directories mode parameter ignored parent_mode
💡 Result:
In older versions of Python (prior to 3.15), the pathlib.Path.mkdir(mode=..., parents=True) method does not apply the specified mode to any intermediate parent directories created; instead, it creates them using default system permissions subject only to the process's umask [1][2][3][4]. This behavior mimics the POSIX mkdir -p command [1][5]. To address this, Python 3.15 introduced the parent_mode parameter [5][6]. When parent_mode is provided, it is used as the mode for any newly created, intermediate-level directories when parents is true (also subject to the process's umask) [5][6]. If you are using a version of Python earlier than 3.15, you cannot specify a mode for parent directories directly via the mkdir call [1][4]. The standard workaround is to iterate over the parent path parts and create them individually: for parent in reversed(path.parents): parent.mkdir(mode=0o770, exist_ok=True) path.mkdir(mode=0o770, parents=False, exist_ok=True) This ensures each directory in the tree is created with your desired permissions [2].
Citations:
- 1: https://docs.python.org/3/library/pathlib.html
- 2: https://stackoverflow.com/questions/67299076/python-pathlib-path-mkdir-applies-desired-mode-to-final-directory-but-mode
- 3: https://docs.python.org/3.10/library/pathlib.html
- 4: https://docs.python.org/release/3.12.1/library/pathlib.html
- 5: https://github.com/python/cpython/blob/main/Doc/library/pathlib.rst
- 6: https://github.com/python/cpython/blob/master/Doc/library/pathlib.rst
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== inspect bootstrap_executable_tools.py around cache paths =="
cat -n src/bootstrap_executable_tools.py | sed -n '80,310p'
echo "== lock/cache path references =="
rg -n "objects|archives|_object_path|ensure_private_directory|bootstrap-cache|transaction|stage_root|stage" src docs || trueRepository: 0cwa/PixeneOS
Length of output: 16570
Create intermediate bootstrap cache directories privately.
mkdir(parents=True) may create missing parents with default permissions and rejects existing parents with FileExistsError, so bootstrap-cache/objects/archives/<digest> does not keep the documented mode-0700 property for all cache components. Create each component explicitly with mode 0o700 instead of relying on parents=True.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/bootstrap_io.py` around lines 29 - 36, Update ensure_private_directory to
create each missing path component explicitly with mode 0o700 rather than using
mkdir(parents=True). Preserve existing-directory handling, then validate every
component as a private real directory so the entire bootstrap cache path
maintains the documented permissions.
| def test_sealed_memfd_executes_a_real_native_binary(self) -> None: | ||
| payload = Path("/usr/bin/true").read_bytes() | ||
| layout = [locked_member("tool", payload)] | ||
| with tempfile.TemporaryDirectory() as temporary: | ||
| directory = Path(temporary) / "installed" | ||
| directory.mkdir(mode=0o700) | ||
| tool = directory / "tool" | ||
| tool.write_bytes(payload) | ||
| tool.chmod(0o755) | ||
| child = os.fork() | ||
| if child == 0: | ||
| try: | ||
| with open_validated_executable(directory, layout, "tool") as source: | ||
| with open_sealed_executable(source, layout[0]) as executable: | ||
| os.execve(executable, ["tool"], {"PATH": "/usr/bin"}) | ||
| finally: | ||
| os._exit(127) | ||
| _, status = os.waitpid(child, 0) | ||
| self.assertEqual(os.waitstatus_to_exitcode(status), 0) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Guard the native-execution test against missing prerequisites.
The test depends on two environment facts that it does not check.
/usr/bin/true is absent on some images; coreutils may install it at /bin/true only. Line 415 then raises FileNotFoundError and the test errors.
os.execve with a descriptor requires os.execve in os.supports_fd. src/bootstrap_executable_tools.py guards this at line 373 with fd_exec_supported. This test does not. On a platform without descriptor execution, the child exits 127 and the assertion reports a product defect that does not exist.
Skip the test when either prerequisite is absent.
💚 Proposed fix to add skip guards
+ `@unittest.skipUnless`(
+ os.execve in os.supports_fd, "descriptor execution is unavailable"
+ )
+ `@unittest.skipUnless`(
+ Path("/usr/bin/true").is_file(), "/usr/bin/true is unavailable"
+ )
def test_sealed_memfd_executes_a_real_native_binary(self) -> None:
payload = Path("/usr/bin/true").read_bytes()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def test_sealed_memfd_executes_a_real_native_binary(self) -> None: | |
| payload = Path("/usr/bin/true").read_bytes() | |
| layout = [locked_member("tool", payload)] | |
| with tempfile.TemporaryDirectory() as temporary: | |
| directory = Path(temporary) / "installed" | |
| directory.mkdir(mode=0o700) | |
| tool = directory / "tool" | |
| tool.write_bytes(payload) | |
| tool.chmod(0o755) | |
| child = os.fork() | |
| if child == 0: | |
| try: | |
| with open_validated_executable(directory, layout, "tool") as source: | |
| with open_sealed_executable(source, layout[0]) as executable: | |
| os.execve(executable, ["tool"], {"PATH": "/usr/bin"}) | |
| finally: | |
| os._exit(127) | |
| _, status = os.waitpid(child, 0) | |
| self.assertEqual(os.waitstatus_to_exitcode(status), 0) | |
| `@unittest.skipUnless`( | |
| os.execve in os.supports_fd, "descriptor execution is unavailable" | |
| ) | |
| `@unittest.skipUnless`( | |
| Path("/usr/bin/true").is_file(), "/usr/bin/true is unavailable" | |
| ) | |
| def test_sealed_memfd_executes_a_real_native_binary(self) -> None: | |
| payload = Path("/usr/bin/true").read_bytes() | |
| layout = [locked_member("tool", payload)] | |
| with tempfile.TemporaryDirectory() as temporary: | |
| directory = Path(temporary) / "installed" | |
| directory.mkdir(mode=0o700) | |
| tool = directory / "tool" | |
| tool.write_bytes(payload) | |
| tool.chmod(0o755) | |
| child = os.fork() | |
| if child == 0: | |
| try: | |
| with open_validated_executable(directory, layout, "tool") as source: | |
| with open_sealed_executable(source, layout[0]) as executable: | |
| os.execve(executable, ["tool"], {"PATH": "/usr/bin"}) | |
| finally: | |
| os._exit(127) | |
| _, status = os.waitpid(child, 0) | |
| self.assertEqual(os.waitstatus_to_exitcode(status), 0) |
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 426-427: Use a single with statement with multiple contexts instead of nested with statements
(SIM117)
[error] 428-428: Starting a process without a shell
(S606)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_bootstrap_archive.py` around lines 414 - 432, Update
test_sealed_memfd_executes_a_real_native_binary to locate the native true binary
using /usr/bin/true with /bin/true as a fallback, and skip the test if neither
exists. Also skip when os.execve is not present in os.supports_fd before
forking, preserving the existing execution assertions when both prerequisites
are available.
Summary
Dependency
This PR is intended to follow 0cwa/my-avbroot-setup#4, which syncs upstream 848deb1 and preserves the 0cwa compatibility layer.
Validation
The existing upstream maintenance PR pixincreate#336 and #59 were not merged wholesale because they would select the upstream helper and discard fork-specific behavior.