diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ee29c4c..f9a0ff1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,9 +4,10 @@ on: schedule: - cron: "0 0 * * 1" # A merge that changes a kernel config snippet changes the built kernel - # without bumping any version, so republish the release set now rather than - # waiting for the weekly cron. On main the tags carry no branch suffix, so - # this overwrites the canonical release tags with the new config content. + # without moving any kernel branch, so `new` would see nothing to do. Force a + # rebuild now rather than waiting for the weekly cron. On main the tags carry + # no branch suffix, so this overwrites the canonical tags with the new config + # content. push: branches: - main @@ -17,7 +18,7 @@ on: spec: description: 'Build Specification' type: string - default: "only-latest-lts" + default: "new" required: true publish: description: 'Publish Builds' @@ -35,8 +36,9 @@ jobs: uses: ./.github/workflows/matrix.yml secrets: inherit with: - # schedule/push carry no inputs, so this fallback is what the cron and - # the configs/** push actually build. Keep it equal to the - # workflow_dispatch default above. - spec: ${{ inputs.spec || 'only-latest-lts' }} + # schedule/push carry no inputs, so these fallbacks are what the cron and + # the configs/** push actually build. The cron only wants branches that + # have moved (`new`); a configs/** merge changed the build inputs without + # moving anything, so it has to force `rebuild`. + spec: ${{ inputs.spec || (github.event_name == 'push' && 'rebuild' || 'new') }} publish: ${{ inputs.publish == '' || inputs.publish }} diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml index fafc667a..5a489eb7 100644 --- a/.github/workflows/matrix.yml +++ b/.github/workflows/matrix.yml @@ -52,8 +52,9 @@ jobs: run: | # Split matrix.json into two strategy-matrix-shaped outputs: one keyed # on `builds` (per-arch build jobs) and one on `merges` (per - # (version, flavor) manifest-list assembly jobs). Each iteration of the - # build job sees matrix.builds.* and each merge sees matrix.merges.*. + # (branch, version, flavor) manifest-list assembly jobs). Each iteration + # of the build job sees matrix.builds.* and each merge sees + # matrix.merges.*. BUILDS=$(jq -c '{builds: .builds}' matrix.json) MERGES=$(jq -c '{merges: .merges}' matrix.json) echo "builds=${BUILDS}" >> "${GITHUB_OUTPUT}" @@ -62,7 +63,7 @@ jobs: builds: "${{ steps.capture-matrix.outputs.builds }}" merges: "${{ steps.capture-matrix.outputs.merges }}" build: - name: "build ${{ matrix.builds.version }} ${{ matrix.builds.flavor }} ${{ matrix.builds.arch }}" + name: "build ${{ matrix.builds.branch }} ${{ matrix.builds.version }} ${{ matrix.builds.flavor }} ${{ matrix.builds.arch }}" needs: matrix strategy: fail-fast: false @@ -72,6 +73,9 @@ jobs: KERNEL_PUBLISH: "${{ inputs.publish }}" KERNEL_VERSION: "${{ matrix.builds.version }}" KERNEL_SRC_URL: "${{ matrix.builds.source }}" + KERNEL_SRC_REPO: "${{ matrix.builds.repo }}" + KERNEL_SRC_REF: "${{ matrix.builds.ref }}" + KERNEL_SRC_COMMIT: "${{ matrix.builds.commit }}" FIRMWARE_URL: "${{ matrix.builds.firmware_url }}" FIRMWARE_SIG_URL: "${{ matrix.builds.firmware_sig_url }}" KERNEL_FLAVOR: "${{ matrix.builds.flavor }}" @@ -158,7 +162,7 @@ jobs: - name: upload docker script uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: "build-${{ matrix.builds.version }}-${{ matrix.builds.flavor }}-${{ matrix.builds.arch }}.sh" + name: "build-${{ matrix.builds.branch }}-${{ matrix.builds.version }}-${{ matrix.builds.flavor }}-${{ matrix.builds.arch }}.sh" path: "docker.sh" compression-level: 0 - name: run docker script @@ -174,7 +178,7 @@ jobs: if: ${{ inputs.publish }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: "digests-${{ matrix.builds.version }}-${{ matrix.builds.flavor }}-${{ matrix.builds.arch }}" + name: "digests-${{ matrix.builds.branch }}-${{ matrix.builds.version }}-${{ matrix.builds.flavor }}-${{ matrix.builds.arch }}" path: "digests.json" if-no-files-found: error compression-level: 0 @@ -182,7 +186,7 @@ jobs: merge: # Stitch the per-arch single-platform pushes from `build` into multi-arch # manifest lists. Only runs when publishing; no-op when nothing was pushed. - name: "merge ${{ matrix.merges.version }} ${{ matrix.merges.flavor }}" + name: "merge ${{ matrix.merges.branch }} ${{ matrix.merges.version }} ${{ matrix.merges.flavor }}" needs: [matrix, build] if: ${{ inputs.publish && needs.matrix.outputs.merges != '' }} strategy: @@ -197,6 +201,9 @@ jobs: KERNEL_FLAVOR: "${{ matrix.merges.flavor }}" KERNEL_PRODUCES: "${{ join(matrix.merges.produces, ',') }}" KERNEL_SRC_URL: "${{ matrix.merges.source }}" + KERNEL_SRC_REPO: "${{ matrix.merges.repo }}" + KERNEL_SRC_REF: "${{ matrix.merges.ref }}" + KERNEL_SRC_COMMIT: "${{ matrix.merges.commit }}" FIRMWARE_URL: "${{ matrix.merges.firmware_url }}" DIGESTS_DIR: digests steps: @@ -230,14 +237,14 @@ jobs: # Each per-arch build uploads its digests under a uniquely-named # artifact; pattern + default merge-multiple=false drops each artifact # into its own subdirectory under digests/. - pattern: "digests-${{ matrix.merges.version }}-${{ matrix.merges.flavor }}-*" + pattern: "digests-${{ matrix.merges.branch }}-${{ matrix.merges.version }}-${{ matrix.merges.flavor }}-*" path: digests - name: generate merge script run: uv run ./hack/build/generate-merge-script.py - name: upload merge script uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: "merge-${{ matrix.merges.version }}-${{ matrix.merges.flavor }}.sh" + name: "merge-${{ matrix.merges.branch }}-${{ matrix.merges.version }}-${{ matrix.merges.flavor }}.sh" path: "merge.sh" compression-level: 0 - name: run merge script diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a169b32..3bcc8c3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,5 +19,5 @@ jobs: uses: ./.github/workflows/matrix.yml secrets: inherit with: - spec: "only-latest-lts:flavor=host,zone,zone-nvidiagpu" + spec: "rebuild:branch=6.18-lts;flavor=host,zone,zone-nvidiagpu" publish: false diff --git a/Dockerfile b/Dockerfile index b7a4430e..c6737170 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,9 @@ +# KERNEL_SRC_URL is an archive of one commit of the Edera Linux tree (see +# config.yaml and hack/build/matrix.py). It is addressed by commit, so this +# ADD's buildkit cache entry can never go stale against a moving branch. FROM --platform=$BUILDPLATFORM scratch AS kernelsrc ARG KERNEL_SRC_URL= -ADD ${KERNEL_SRC_URL} /src.tar.xz +ADD ${KERNEL_SRC_URL} /src.tar.gz FROM --platform=$BUILDPLATFORM scratch AS firmware ARG FIRMWARE_URL= @@ -24,7 +27,7 @@ WORKDIR /build RUN chmod +x hack/build/docker-build-internal.sh FROM buildenv AS build-staged -COPY --from=kernelsrc --chown=build:build /src.tar.xz /build/override-kernel-src.tar.xz +COPY --from=kernelsrc --chown=build:build /src.tar.gz /build/override-kernel-src.tar.gz FROM build-staged AS build-staged-amdgpu COPY --from=firmware --chown=build:build /firmware.tar.xz /build/override-firmware.tar.xz diff --git a/README.md b/README.md index 02f09c79..8962f7cc 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,74 @@ Builds the Linux kernel into OCI images. ## Build overview +Kernels are built from [github.com/edera-dev/linux](https://github.com/edera-dev/linux), Edera's downstream +Linux tree. Every Edera change is a commit on a branch there, so this repo carries no patch series of its +own: whatever the branch says is what gets built. + This repo is a series of helper scripts and Github Actions that -1. Use [config.yaml](/config.yaml) and various scripts in `hack/build` to generate a matrix of kernel upstream versions, variants, and flavors to be built. -1. Fetches `kernel.org` tarballs, applies custom config flags, and patches, depending on version, flavor and variant. -1. Builds those kernels for x86_64/arm64. +1. Resolve each branch listed in [config.yaml](/config.yaml) to the commit it currently points at, and read + the kernel version out of that commit's `Makefile`. +1. Use that, plus the flavors and architectures in `config.yaml`, to generate a build matrix. +1. Fetch the source archive for that exact commit, apply the Edera kconfig fragments, and build for + x86_64/aarch64. -### Variant and flavor configs +### Branches -See the [configs](/configs/README.md) directory for more info. +| Branch in `config.yaml` | Ref on `edera-dev/linux` | What it is | +| --- | --- | --- | +| `6.18-lts` | `edera/6.18-lts` | The 6.18 LTS series with the Edera stack. Also publishes `latest`. | +| `mainline` | `edera/mainline` | Current mainline (including `-rc`) with the Edera stack. | + +Adding a branch is three lines in `config.yaml`; nothing else needs to know about it. + +### Image tags + +Each build publishes one immutable tag and a set of moving aliases. For example, `edera/6.18-lts` at +6.18.52, commit `efb09285bd95`: + +| Tag | Moves? | +| --- | --- | +| `zone-kernel:6.18.52-gefb09285bd95` | no - one commit, forever | +| `zone-kernel:6.18.52` | yes | +| `zone-kernel:6.18` | yes | +| `zone-kernel:6.18-lts` | yes | +| `zone-kernel:latest` | yes | + +and `edera/mainline` at 7.3-rc3, commit `391f6f12ecf5`, publishes `7.3.0-rc3-g391f6f12ecf5`, `7.3.0-rc3` and +`mainline`. A prerelease deliberately does not claim the bare series tag (`7.3`), which belongs to the +eventual 7.3 release. + +The immutable tag names the *source tree*, not every build input. A change to a kconfig fragment in +[configs](/configs) rebuilds and republishes the same tags against the same commit; the kernel inside +changes, the tag does not. `config.gz` and the image's `metadata` (which records +`KERNEL_SRC_REPO`/`KERNEL_SRC_REF`/`KERNEL_SRC_COMMIT` alongside a hash of the kconfig) are what +distinguish two such builds. + +### Build specifications + +The `Build Kernels` action takes a build spec of `[:]`: -### Patches +- `new` (default) - build only what the registry does not already have. Since each build carries an + immutable `-g` tag, this means "build every branch that has moved". This is what the + weekly cron runs. +- `rebuild` - build everything the config selects, published or not. Use this when something other than the + kernel source changed: a kconfig fragment, the buildenv, the packaging. A merge touching `configs/**` + triggers this automatically. -See the [patches](/patches) directory for the current set of patches Edera carries against upstream kernels. +Constraints are semicolon-separated `key=value` pairs over `branch`, `flavor` and `arch`, with +comma-separated values: -Note that not all the patches will be applied to all kernel versions, this is driven by version constraints in [config.yaml](/config.yaml). +``` +new +rebuild:branch=mainline +rebuild:branch=6.18-lts;flavor=host,zone +new:flavor=zone;arch=aarch64 +``` + +### Variant and flavor configs + +See the [configs](/configs/README.md) directory for more info. ## Building your own kernels with custom KConfig (Using Github Actions) @@ -30,14 +83,14 @@ Note that not all the patches will be applied to all kernel versions, this is dr 1. Edit the line `imageNameFormat: "ghcr.io/edera-dev/[image]:[tag]"` and change it to `imageNameFormat: "ghcr.io//[image]:[tag]"` 1. Add or remove any `Kconfig` options you want to the `flavor` and `variant` Kconfig fragments in [configs](/configs), as outlined by the [README](/configs/README.md) in that folder. 1. Commit those changes to `main` in your fork. -1. From your forked repository's `Actions` tab, run the `Build Kernels` job with a Build Specification like: `stable:flavor=zone,host`. This will build `zone` and `host` kernel flavors using `stable` kernel.org releases for all current LTS kernels. +1. From your forked repository's `Actions` tab, run the `Build Kernels` job with a Build Specification like: `rebuild:flavor=zone,host`. This will build the `zone` and `host` flavors of every branch in `config.yaml`. 1. ![Example](/images/job-example.png) ## Building your own kernels with custom KConfig (Locally, for debugging) For most of the kernels in this registry, debugging symbols and features are disabled, to keep the kernel artifacts small. -You may want or need to build your own debugging kernel with custom patches/options locally, and publish it to a transient OCI registry (like [ttl.sh](ttl.sh)) for testing purposes. +You may want or need to build your own debugging kernel with custom options locally, and publish it to a transient OCI registry (like [ttl.sh](ttl.sh)) for testing purposes. To do this, you will need `docker` installed and configured correctly to support cross-builds (`docker buildx`) in your local environment. @@ -46,42 +99,56 @@ The simplest way to do that is to 1. Clone this repo locally: `git clone git@github.com:edera-dev/linux-kernel-oci.git` 1. Manually edit [config.yaml](/config.yaml) on-disk: - to change the `imageNameFormat` key to push to an OCI registry you have access to. - - to change the `architectures` YAML key to only include the architectures you care about (x86_64, arm64, or both - `docker buildx` is used so you can build arm64 on x86_64 and vice-versa). + - to change the `architectures` YAML key to only include the architectures you care about (x86_64, aarch64, or both - `docker buildx` is used so you can build aarch64 on x86_64 and vice-versa). - to change the `flavors` YAML key to only include the flavors you care about (host, or zone, or both). - - to change the `versions` YAML key to only include the `kernel.org` versions you care about. For instance, to only build the latest `5.4` kernel.org upstream release, use: - - ```yaml - versions: - - series: '5.4' - ``` - - - For example, if I wanted to only build the `zone` kernel flavor, only for `x86_64`, only the latest `6.15` point release, and tag the result for a custom `ttl.sh/hackben` registry, the final result would look something like this: + - to change the `branches` YAML key to only include the branches you care about. You can also point + `source.repo` at your own fork of `edera-dev/linux` and list a branch on it. + - For example, if I wanted to only build the `zone` kernel flavor, only for `x86_64`, only from the LTS + branch, and tag the result for a custom `ttl.sh/hackben` registry, the final result would look + something like this: ```yaml imageNameFormat: "ttl.sh/hackben/[image]:[tag]" + source: + repo: https://github.com/edera-dev/linux + branches: + - name: 6.18-lts + ref: edera/6.18-lts architectures: - x86_64 flavors: - - name: zone-debu - constraints: - series: - - '6.15' - versions: - - current: true + - name: zone ``` 1. Add or remove any `Kconfig` options you want to the `flavor` and `variant` Kconfig fragments in [configs](/configs), as outlined by the [README](/configs/README.md) in that folder. 1. Run [hack/build/docker-build.sh](hack/build/docker-build.sh) - - It is **important** you follow the previous step, and edit the [config.yaml](config.yaml) locally to reduce the number of kernels the script will try to build, or you may end up building 15+ different kernels in parallel on your local box, which will take a very, very long time. + - It is **important** you follow the previous step, and edit the [config.yaml](config.yaml) locally to reduce the number of kernels the script will try to build, or you may end up building many different kernels in parallel on your local box, which will take a very, very long time. - When this command runs, it will generate a build matrix and print out what it will build. + - Pass a build spec as the first argument to narrow it further, e.g. + `./hack/build/docker-build.sh 'rebuild:branch=6.18-lts;flavor=zone'` (quote it - `;` is a shell + metacharacter). 1. When the above command finishes, you can see the local OCI images that were built by running `docker image list`. The images will be tagged with the repo you specified in `imageNameFormat` in the [config.yaml](/config.yaml). 1. From this point, you may push those images to an OCI registry with standard commands like `docker image push `, and consume them how you wish. 1. If you wish to unpack and inspect the final image (for instance, to make sure certain modules or firmware exist in the correct paths, or that the final `config.gz` has the options you expect), you can do the following to fetch and extract the image artifact you just pushed to your local disk with [`crane`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md): ```sh - crane export ttl.sh/hackben/zone-kernel:6.15.6 - --platform=linux/amd64 | tar --keep-directory-symlink -xf - -C . + crane export ttl.sh/hackben/zone-kernel:6.18-lts - --platform=linux/amd64 | tar --keep-directory-symlink -xf - -C . cd `kernel` zcat config.gz + cat metadata unsquashfs addons.squashfs ... ``` + +### Building a branch that isn't in `config.yaml` + +`hack/build/build.sh` takes `KERNEL_SRC_URL` directly and understands a `git::[::]` form, which +clones that ref instead of fetching an archive. That is the escape hatch for building a work-in-progress +branch without listing it in `config.yaml` first. It expects the kbuild toolchain to already be present, so +run it inside the build environment image (`ghcr.io/edera-dev/kernel-buildenv`) rather than on a bare host: + +```sh +KERNEL_VERSION=6.18.52 KERNEL_FLAVOR=zone \ + KERNEL_SRC_URL='git::https://github.com/edera-dev/linux::azenla/zone-perf' \ + ./hack/build/build.sh +``` diff --git a/config.yaml b/config.yaml index 27ce25d6..508e54b6 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,34 @@ imageNameFormat: "ghcr.io/edera-dev/[image]:[tag]" + +# Kernel sources come from Edera's downstream Linux tree. Every branch listed +# below carries the full Edera patch stack as commits, so this repo no longer +# fetches kernel.org tarballs and no longer carries a patch series of its own: +# what the branch says is what gets built. Adding or dropping a patch is a +# commit on edera-dev/linux, not a change here. +source: + repo: https://github.com/edera-dev/linux + +# Each branch produces one build per (flavor, architecture). The kernel version +# comes from the branch's Makefile at the resolved commit, so it tracks whatever +# the branch has been rebased onto without anything here needing an edit. +# +# Every build publishes an immutable `-g` tag plus moving +# aliases: ``, the `.` series (release kernels only -- a +# prerelease must not claim the series tag its final release will want), the +# branch name, and anything in `aliases`. +branches: +- name: 6.18-lts + ref: edera/6.18-lts + # The LTS branch is what a consumer that pins nothing should get. + aliases: + - latest +- name: mainline + ref: edera/mainline + # Default set of architectures applied to any flavor that does not override it. architectures: - x86_64 + flavors: - name: zone # zone is the only flavor we currently publish for aarch64. Other flavors fall @@ -10,11 +37,7 @@ flavors: architectures: - x86_64 - aarch64 - constraints: - lower: '6.1' - name: zone-amdgpu - constraints: - lower: '6.1' - name: zone-nvidiagpu # we will generate distinct images for each of these # local tags. For `zone-nvidiagpu` specifically, @@ -29,156 +52,20 @@ flavors: - 'nvidia-595.45.04' # Nvidia: "Latest Beta Version" - 'nvidia-595.91.07' # Nvidia: "Latest Production Branch Version" constraints: - lower: '6.18' -- name: zone-openpax - constraints: - series: - - '6.10' - - '6.11' + # NVIDIA's open kernel modules trail mainline, so this flavor is pinned to + # the LTS branch -- which is also the only place it has ever actually built. + # Add `mainline` here once the driver series in local_tags compiles against + # whatever edera/mainline is currently rebased onto. + branches: + - 6.18-lts - name: host -versions: -- current: true -- lower: '5.15' -patches: -- patch: hvc-xen-idm-support-1.patch - series: - - '5.10' - upper: 5.10.210 -- patch: hvc-xen-idm-support-2.patch - series: - - '5.10' - lower: '5.10.211' -- patch: 0001-Revert-ALSA-memalloc-Workaround-for-Xen-PV.patch - series: - - '6.6' - - '6.7' - - '6.8' - - '6.9' - - '6.10' - - '6.11' -- patch: openpax-6.10-20240913112548.patch - series: '6.10' - flavors: - - zone-openpax -- patch: openpax-6.11.2-20241009165538.patch - series: '6.11' - flavors: - - zone-openpax -- patch: 0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_01.patch - lower: '6.10' - upper: '6.12.4' -- patch: 0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_02.patch - lower: '6.12.5' - upper: '6.13.999' -- patch: 0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_03.patch - lower: '6.14' -- patches: - - 9pfs-xen-hotplug-1.patch - - 9pfs-xen-hotplug-2.patch - any: - - series: '6.6' - upper: '6.6.63' - - series: '6.12' - upper: '6.12.1' -- patch: hack-around-pci-msix-restore-bugs-in-pv-domu.patch - lower: '6.1' - flavors: - - zone-amdgpu -- patch: 0001-x86-CPU-AMD-avoid-printing-reset-reasons-on-Xen-domU.patch - lower: '6.16' -- patch: 0001-9p-xen-mark-9p-transport-device-as-closing-when-remo.patch - lower: '6.1' - upper: '6.12.74' -# Share one Xen 9pfs frontend across many mounts (each with its own aname) and -# advertise it as edera_multi_attach_v1 in /sys/fs/9p/features, so zones can -# mount each volume as an independent 9p mount and df resolves per volume. -# The 9p mount API became fs_context-based in 6.19, so 6.18.x needs the -# separate old-API backport below. -- patch: 9pfs-xen-multi-attach.patch - lower: '6.19' -- patch: 9pfs-xen-multi-attach-6.18.patch - series: '6.18' -- patch: 0002-x86-amd_node-fix-integer-divide-by-zero-during-init.patch - lower: '6.17' -- patch: 0003-x86-amd_node-fix-null-pointer-dereference-if-amd_smn.patch - lower: '6.17' -- patches: - - 0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch - - 0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch - - 0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch - - 0004-xen-grant-table-add-gnttab_alloc_pages_node.patch - - 0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch - - 0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch - - 0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch - - 0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch - - 0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch - - 0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch - - 0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch - - 0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch - lower: '6.18' -- patches: - - 0001-xen-xlate_mmu-relocate-remap_pfn-utils.patch - - 0002-xen-xlate_mmu-batch-gfn-mappings.patch - lower: '6.10' -- patches: - - hyperv/0001-x86-hyperv-enable-a-Xen-PV-dom0-to-use-the-L0-Hyper-.patch - - hyperv/0002-Drivers-hv-use-host-frames-for-GPADLs-and-SynIC-page.patch - - hyperv/0003-drivers-hv-deliver-the-VMBus-interrupt-via-a-Xen-VIR.patch - - hyperv/0004-hv_netvsc-DMA-map-transmit-pages-when-nested-on-Xen.patch - - hyperv/0005-x86-hyperv-take-vPCI-device-interrupts-through-Xen-w.patch - - hyperv/0006-drivers-hv-refuse-to-balloon-when-nested-on-Xen.patch - series: '6.18' -# Xen PV-IOMMU: let a PV guest program IOMMU contexts through the PV-IOMMU -# hypercall interface, so passed-through devices DMA through a real translation -# context instead of relying on an identity map. Includes the parallel -# io-pgtable used for iova_to_phys, the pcifront machine-BDF lookup the -# hypercalls need, and the PV MSI/MSI-X fixes that go with it. -# -# Pinned to the 6.18 series, not `lower`. iommu_domain_ops::attach_dev gained a -# third `struct iommu_domain *old` argument in 6.19, so this backport only -# compiles against 6.18.x. It is not enough to let the range run open-ended: -# `patch -p1` still applies these to 6.19 with fuzz and the build then fails on -# the incompatible function pointer, so the upper bound has to be explicit. A -# forward-port for 6.19+ wants the three-argument attach_dev and belongs in a -# separate patches/pv-iommu/ directory alongside this one. -# -# These files are trimmed relative to the edera/azenla/feat/pv-iommu branch they -# come from: commit 1 there also carries stray hunks belonging to two other -# series -- the VIRQ_HYPERV_* defines in include/xen/interface/xen.h and the -# XENMEM_get_mfn_pxms block in include/xen/interface/memory.h -- which this tree -# already supplies from patches/hyperv/ and -# 0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch. Both are -# dropped here. If you regenerate these patches from that branch, drop them -# again or the 6.18 stack collides on those two headers. -- patches: - - pv-iommu-6.18/0001-iommu-xen-Add-Xen-PV-IOMMU-driver.patch - - pv-iommu-6.18/0002-iommu-xen-Allow-the-PV-IOMMU-driver-in-a-guest.patch - - pv-iommu-6.18/0003-iommu-xen-Give-the-identity-domain-a-type-and-ops.patch - - pv-iommu-6.18/0004-iommu-dma-Don-t-reserve-PCI-windows-under-a-paravirt.patch - - pv-iommu-6.18/0005-xen-pcifront-Let-callers-ask-for-a-device-s-machine-.patch - - pv-iommu-6.18/0006-iommu-xen-Default-a-PV-guest-to-the-identity-context.patch - - pv-iommu-6.18/0007-PCI-MSI-don-t-write-the-MSI-X-table-in-a-Xen-PV-gues.patch - - pv-iommu-6.18/0008-iommu-xen-batch-map-subops-over-contiguous-runs.patch - - pv-iommu-6.18/0009-iommu-xen-do-not-claim-the-IOMMU-itself.patch - - pv-iommu-6.18/0010-x86-xen-disable-the-right-interrupt-type-when-tearin.patch - - pv-iommu-6.18/0011-iommu-xen-flush-the-IOTLB-once-per-mapping-not-once-.patch - series: '6.18' -- patch: 0001-feat-xen-deflate-balloon-via-oom-notifier.patch - lower: '6.18' -# virtio-gpu decides between DMA and guest-physical addresses on its own, -# without the xen_domain() case vring_use_dma_api() has, so a Xen PV domain -# hands the host addresses that resolve to somebody else's memory and the -# display scans out garbage. This adds the xen_domain() check to fix that. -- patch: 0001-drm-virtio-use-the-DMA-API-for-resource-backing-on-X.patch - series: '6.18' -# qemu_fw_cfg's DMA interface hands the device a virt_to_phys() address, which a -# Xen PV domain's pseudo-physical address is not. The device never clears the -# control word and the probe spins in fw_cfg_wait_for_control() forever, in a -# loop that never dequeues a signal, so the udev worker cannot be killed and -# burns a CPU until the domain is reset. Refuse the DMA interface on PV; reads -# use the data register and still work. -- patch: 0001-firmware-qemu_fw_cfg-do-not-use-the-DMA-interface-on.patch - series: '6.18' + +# zone-openpax is parked: the openpax series has not been restored on the Edera +# branches yet. Its kconfig fragments are still in configs/, so re-enabling it +# is a matter of uncommenting this entry once the series lands (and constraining +# it to the branches that carry it). +# - name: zone-openpax + images: - target: kernelsrc name: kernel-src @@ -197,6 +84,7 @@ images: - target: sdk name: "[flavor]-kernel-sdk" format: kernel.sdk + runners: # Match order matters: first runner whose constraints match wins. - name: ubuntu-24.04-arm diff --git a/configs/README.md b/configs/README.md index c0d060d8..0179a73a 100644 --- a/configs/README.md +++ b/configs/README.md @@ -7,30 +7,30 @@ and (if selected) any variant configs on top of that, relying on the kernel's `m `flavors` are the different basic kernels we ship. `variants` are variations on those `flavors`. -- The `flavor` configs are expressed as delta fragments from the upstream kernel's default config for the given architecture. +- The `flavor` configs are expressed as delta fragments from the kernel branch's default config for the given architecture. - The `variant` configs are expressed as delta fragments from the `flavor` configs for the given architecture. When a kernel is built, we take the flavor config, and any variants (if defined) and overlay them on top of the default config. The result is the actual config the kernels are built with. - We currently have two kernel `flavors` - `zone` and `host` -- We currently have two `zone` flavor `variants`, `zone-amdgpu` and `zone-openpax` +- We currently ship two `zone` flavor `variants`, `zone-amdgpu` and `zone-nvidiagpu`. A third, `zone-openpax`, is parked until the openpax series is restored on the Edera kernel branches; its fragments are still here. ## Generating trimmed `flavor` configs -A "flavor config" is an Edera config fragment that contains only the Edera-specific changes from the latest stable _upstream_ kernel's default config file, for a given architecture. -Example: we want to generate a clean/updated `zone.config` against the latest default upstream 6.14.6 kernel config for x86_64 -1. Run `./hack/build/generate-clean-flavor-config.sh 6.14.6 x86_64 configs/x86_64/zone.config my-updated-zone.config` -1. `my-updated-zone.config` should only have the kernel config options that are _not_ in the x86_64 default config for linux 6.14.6 now. +A "flavor config" is an Edera config fragment that contains only the Edera-specific changes from the default config of an [Edera kernel branch](https://github.com/edera-dev/linux), for a given architecture. +Example: we want to generate a clean/updated `zone.config` against the `edera/6.18-lts` default config for x86_64 +1. Run `./hack/build/generate-clean-flavor-config.sh edera/6.18-lts x86_64 configs/x86_64/zone.config my-updated-zone.config` +1. `my-updated-zone.config` should only have the kernel config options that are _not_ in the x86_64 default config on that branch now. 1. `cp my-updated-zone.config configs/x86_64/zone.config` and check in. 1. ditto for `host.config`, and `aarch64`. -1. It is strongly recommended to use the _oldest_ kernel version Edera currently supports as the base version, as newer kernels commonly add new defaults that were optional in older kernels. +1. It is strongly recommended to use the _oldest_ branch Edera currently builds (today `edera/6.18-lts`) as the base, as newer kernels commonly add new defaults that were optional in older kernels. ## Generating trimmed `variant` configs A "variant config" is an Edera config fragment that contains only the Edera-specific changes from the `flavor` config it belongs to, for a given architecture. Example: we want to generate a clean/updated `zone-amdgpu.config` from the latest `zone.config` -1. Run `./hack/build/generate-clean-variant-config.sh 6.14.6 amd64 configs/x86_64/zone-amdgpu.fragment.config my-updated-zone-admgpu.fragment.config` -1. This will fetch & extract the latest full flavor `config` from `ghcr.io/edera-dev/zone-kernel:6.14.6`. +1. Run `./hack/build/generate-clean-variant-config.sh zone 6.18-lts amd64 configs/x86_64/zone-amdgpu.fragment.config my-updated-zone-admgpu.fragment.config` +1. This will fetch & extract the full flavor `config` from `ghcr.io/edera-dev/zone-kernel:6.18-lts`. 1. `my-updated-zone-admgpu.fragment` should only have the kernel config options that are _not_ in the extracted full flavor config now. 1. `cp my-updated-zone-admgpu.fragment configs/x86_64/zone-amdgpu.fragment.config` -1. It is strongly recommended to use the *oldest* kernel version Edera currently supports as the base version, as newer kernels commonly add new defaults that were optional in older kernels. +1. It is strongly recommended to use the *oldest* branch Edera currently builds as the base, as newer kernels commonly add new defaults that were optional in older kernels. diff --git a/hack/build/build.sh b/hack/build/build.sh index dc9f0cc8..65cace28 100755 --- a/hack/build/build.sh +++ b/hack/build/build.sh @@ -120,6 +120,12 @@ rm -rf "${SDK_OUTPUT_PATH}" echo "KERNEL_VERSION=${KERNEL_VERSION}" echo "KERNEL_UNAME_R=${KERNEL_MODULES_VER}" echo "KERNEL_FLAVOR=${KERNEL_FLAVOR}" + # The exact tree this kernel was built from. There is no patch series on top + # of it -- every Edera change is a commit on that branch -- so this plus the + # config hash below fully describes what is in the image. + echo "KERNEL_SRC_REPO=${KERNEL_SRC_REPO}" + echo "KERNEL_SRC_REF=${KERNEL_SRC_REF}" + echo "KERNEL_SRC_COMMIT=${KERNEL_SRC_COMMIT}" sha256sum "${KERNEL_OBJ}/.config" | awk '{print "KERNEL_CONFIG=sha256:"$1}' } >"${METADATA_PATH}" gzip -9 <"${KERNEL_OBJ}/.config" >"${CONFIG_GZ_PATH}" diff --git a/hack/build/cdn-url.sh b/hack/build/cdn-url.sh deleted file mode 100755 index eba3edfc..00000000 --- a/hack/build/cdn-url.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -set -e - -if [ -z "${1}" ]; then - echo "Usage: cdn-url.sh " >&2 - exit 1 -fi - -KERNEL_VERSION="${1}" -MAJOR_VERSION="$(echo "${KERNEL_VERSION}" | awk -F '.' '{print $1}')" -MINOR_VERSION="$(echo "${KERNEL_VERSION}" | awk -F '.' '{print $2}')" -PATCH_VERSION="$(echo "${KERNEL_VERSION}" | awk -F '.' '{print $3}')" - -if [ "${PATCH_VERSION}" = "0" ]; then - KERNEL_VERSION="${MAJOR_VERSION}.${MINOR_VERSION}" -fi - -echo "https://cdn.kernel.org/pub/linux/kernel/v${MAJOR_VERSION}.x/linux-${KERNEL_VERSION}.tar.xz" diff --git a/hack/build/common.sh b/hack/build/common.sh index 0c813975..f5c3e2cd 100644 --- a/hack/build/common.sh +++ b/hack/build/common.sh @@ -28,8 +28,13 @@ if [ -z "${KERNEL_VERSION}" ]; then exit 1 fi +# Kernel sources come from Edera's downstream tree (see config.yaml); there is +# no upstream fallback to derive a URL from, so this must be supplied. The +# matrix hands it to us as an immutable commit archive; the Dockerfile stages +# that into the build container as a local file. if [ -z "${KERNEL_SRC_URL}" ]; then - KERNEL_SRC_URL="$(./hack/build/cdn-url.sh "${KERNEL_VERSION}")" + echo "ERROR: KERNEL_SRC_URL must be specified." >&2 + exit 1 fi if [ -z "${KERNEL_FLAVOR}" ]; then @@ -44,49 +49,36 @@ if [ -z "${KERNEL_BUILD_JOBS}" ]; then KERNEL_BUILD_JOBS="$((KERNEL_BUILD_JOBS + 1))" fi -# In the case of a stable release, e.g. 6.10.7, this becomes 6.10. -MAINLINE_VERSION="${KERNEL_VERSION%.*}" -# In the case of a mainline release, e.g. 6.10, this will collapse to -# 6 -> 6. In that case, ${KERNEL_VERSION} is the mainline version. -if [ "${MAINLINE_VERSION}" = "${MAINLINE_VERSION%.*}" ]; then - MAINLINE_VERSION="${KERNEL_VERSION}" -fi - +# KERNEL_SRC_URL is one of: +# - a path to a local source archive (what CI uses: the Dockerfile ADDs the +# commit archive into the build container, and the compile step points here +# at that file), +# - an http(s) URL to a source archive, +# - "git::[::]", which clones directly. Handy locally for +# building a work-in-progress branch without pushing it anywhere. +# +# No patches are applied either way: every Edera change is a commit on the +# branch being built, so the tree that lands here is the tree that gets +# compiled. if [ ! -f "${KERNEL_SRC}/Makefile" ]; then rm -rf "${KERNEL_SRC}" mkdir -p "${KERNEL_SRC}" - KERNEL_SRC_IS_TAR="1" - if [ ! -f "${KERNEL_SRC_URL}" ]; then - if echo "${KERNEL_SRC_URL}" | grep -E '^git::' >/dev/null; then - KERNEL_SRC_IS_TAR="0" - KERNEL_GIT_URL="$(echo "${KERNEL_SRC_URL}" | awk -F '::' '{print $2}')" - KERNEL_GIT_REF="$(echo "${KERNEL_SRC_URL}" | awk -F '::' '{print $3}')" - if [ -z "${KERNEL_GIT_REF}" ]; then - KERNEL_GIT_REF="master" - fi - git clone "${KERNEL_GIT_URL}" -b "${KERNEL_GIT_REF}" "${KERNEL_SRC}" - else - curl --progress-bar -Lf -o "${KERNEL_SRC}.txz" "${KERNEL_SRC_URL}" + if [ -f "${KERNEL_SRC_URL}" ]; then + mv "${KERNEL_SRC_URL}" "${KERNEL_SRC}.tar" + tar xf "${KERNEL_SRC}.tar" --strip-components 1 -C "${KERNEL_SRC}" + rm "${KERNEL_SRC}.tar" + elif echo "${KERNEL_SRC_URL}" | grep -E '^git::' >/dev/null; then + KERNEL_GIT_URL="$(echo "${KERNEL_SRC_URL}" | awk -F '::' '{print $2}')" + KERNEL_GIT_REF="$(echo "${KERNEL_SRC_URL}" | awk -F '::' '{print $3}')" + if [ -z "${KERNEL_GIT_REF}" ]; then + KERNEL_GIT_REF="master" fi + git clone --depth 1 "${KERNEL_GIT_URL}" -b "${KERNEL_GIT_REF}" "${KERNEL_SRC}" else - mv "${KERNEL_SRC_URL}" "${KERNEL_SRC}.txz" + curl --progress-bar -Lf -o "${KERNEL_SRC}.tar" "${KERNEL_SRC_URL}" + tar xf "${KERNEL_SRC}.tar" --strip-components 1 -C "${KERNEL_SRC}" + rm "${KERNEL_SRC}.tar" fi - - if [ "${KERNEL_SRC_IS_TAR}" = "1" ]; then - tar xf "${KERNEL_SRC}.txz" --strip-components 1 -C "${KERNEL_SRC}" - rm "${KERNEL_SRC}.txz" - fi - - uv run "hack/build/patchlist.py" "${KERNEL_VERSION}" "${KERNEL_FLAVOR}" | while read -r PATCH_NAME; do - cd "${KERNEL_SRC}" - if [ "${KERNEL_SRC_IS_TAR}" = "1" ]; then - patch --verbose -p1 <"${KERNEL_DIR}/${PATCH_NAME}" - else - git --verbose apply "${KERNEL_DIR}/${PATCH_NAME}" - fi - cd "${KERNEL_DIR}" - done - cd "${KERNEL_DIR}" fi OUTPUT_DIR="${KERNEL_DIR}/target" @@ -187,7 +179,7 @@ CONFIG_GZ_PATH="${OUTPUT_DIR}/config.gz" # shellcheck disable=SC2034 SDK_PATH="${OUTPUT_DIR}/sdk.tar.gz" -# we often build older kernels that have warnings -# this will ensure they are logged but do not -# prevent building of the kernel. +# the trees we build (an -rc in particular) routinely carry warnings that +# upstream has not swept up yet; this keeps them logged without failing the +# build. export EXTRA_CFLAGS="-Wno-error" diff --git a/hack/build/generate-clean-flavor-config.sh b/hack/build/generate-clean-flavor-config.sh index 639dd9c2..c30767c7 100755 --- a/hack/build/generate-clean-flavor-config.sh +++ b/hack/build/generate-clean-flavor-config.sh @@ -1,51 +1,44 @@ #!/bin/sh if [ $# -ne 4 ]; then - cat < + cat < -Fetch an arch-specific kernel default configuration for a specific stable upstream kernel version and compare it -with another (local) config, outputting a delta flavor config containing only the changed/added lines from the upstream kernel default. +Fetch an arch-specific kernel default configuration from a branch of the Edera Linux tree and compare it +with another (local) config, outputting a delta flavor config containing only the changed/added lines from the kernel default. Arguments: - The stable kernel version to fetch (recomemnded oldest-supported kver) + Branch of the Edera Linux tree to compare against (e.g. edera/6.18-lts) Target architecture (x86_64, arm64, arm) Path to the new kernel config file to compare against. May be complete, or a fragment. Path where the fragment containing only modified options will be saved. Example: - $(basename "$0") 5.4.293 x86_64 /edera_.config zone.config + $(basename "$0") edera/6.18-lts x86_64 /edera_.config zone.config Notes: - The Edera flavor config does not have to be a complete kernel config, but starting from a complete Edera flavor config you know boots is recommended. - - The script will fetch the default config for the specified released version from kernel.org CDN + - The branch is resolved to a commit and the defconfig is fetched from that commit, + so the comparison is against an exact tree rather than a moving branch. - Comment lines/unset opts (starting with #) are filtered out - Only lines that were added or changed in are saved - must end in '.config' or kernel make will complain. -EOF + - Run this from the repository root; it reads source.repo out of config.yaml. +USAGE exit 1 fi -UPSTREAM_KVER="$1" +BRANCH_REF="$1" ARCH="$2" FULL_EDERA_FLAVOR_CONFIG="$3" DELTA_EDERA_FLAVOR_CONFIG="$4" -TEMP_DIR="$(mktemp -d)-$UPSTREAM_KVER-$ARCH" - -# Cleanup temp dir always -trap 'rm -rf "$TEMP_DIR"; echo "Cleaning up temporary files..."; exit' INT TERM EXIT - -KERNEL_URL="https://cdn.kernel.org/pub/linux/kernel/v$(echo "$UPSTREAM_KVER" | cut -d. -f1).x/linux-${UPSTREAM_KVER}.tar.xz" -TARBALL_PATH="$TEMP_DIR/linux-${UPSTREAM_KVER}.tar.xz" if [ ! -f "$FULL_EDERA_FLAVOR_CONFIG" ]; then echo "Error: Complete Edera kernel config file does not exist at $FULL_EDERA_FLAVOR_CONFIG!" exit 1 fi -echo "Fetching kernel.org stable kernel default config for version $UPSTREAM_KVER (arch: $ARCH)..." - # Map architecture names to kernel arch names and config paths case "$ARCH" in x86_64) @@ -60,27 +53,39 @@ arm64 | aarch64) ;; esac -# Extract only the config file we need -mkdir -p "$TEMP_DIR/linux" -echo "Downloading released kernel $UPSTREAM_KVER from $KERNEL_URL to $TARBALL_PATH" -if ! curl -sSL "$KERNEL_URL" -o "$TARBALL_PATH"; then - echo "Error: Failed to download kernel version $UPSTREAM_KVER." +SOURCE_REPO="$(awk '/^source:/{in_source=1; next} in_source && /^[^[:space:]]/{exit} in_source && $1 == "repo:" {print $2; exit}' config.yaml)" + +if [ -z "$SOURCE_REPO" ]; then + echo "Error: could not read source.repo from config.yaml (run this from the repository root)" exit 1 fi -if ! tar -xf "$TARBALL_PATH" --strip-components=1 -C "$TEMP_DIR/linux" "linux-${UPSTREAM_KVER}/$CONFIG_SNIP"; then - echo "Error: Failed to extract config file from the tarball." +# Resolve to a commit first: a raw URL built from a branch name containing a +# slash is ambiguous, and a commit pins the comparison to one exact tree. +COMMIT="$(git ls-remote --heads "$SOURCE_REPO" "refs/heads/${BRANCH_REF}" | cut -f1)" + +if [ -z "$COMMIT" ]; then + echo "Error: branch $BRANCH_REF does not exist on $SOURCE_REPO" exit 1 fi -CONFIG_PATH="$TEMP_DIR/linux/$CONFIG_SNIP" +SLUG="${SOURCE_REPO#https://github.com/}" +SLUG="${SLUG%.git}" +CONFIG_URL="https://raw.githubusercontent.com/${SLUG}/${COMMIT}/${CONFIG_SNIP}" + +TEMP_DIR="$(mktemp -d)" +# Cleanup temp dir always +trap 'rm -rf "$TEMP_DIR"; echo "Cleaning up temporary files..."; exit' INT TERM EXIT + +CONFIG_PATH="$TEMP_DIR/defconfig" -if [ -f "$CONFIG_PATH" ]; then - echo "Generating a trimmed delta flavor config between kernel $UPSTREAM_KVER default config for $ARCH and flavor config $FULL_EDERA_FLAVOR_CONFIG" - ./hack/build/generate-kfragment.sh "$CONFIG_PATH" "$FULL_EDERA_FLAVOR_CONFIG" "$DELTA_EDERA_FLAVOR_CONFIG" -else - echo "Error: Config file not found at $CONFIG_PATH" +echo "Fetching $CONFIG_SNIP from $BRANCH_REF ($COMMIT) for $ARCH..." +if ! curl -sSLf "$CONFIG_URL" -o "$CONFIG_PATH"; then + echo "Error: Failed to download $CONFIG_URL." exit 1 fi +echo "Generating a trimmed delta flavor config between $BRANCH_REF's default config for $ARCH and flavor config $FULL_EDERA_FLAVOR_CONFIG" +./hack/build/generate-kfragment.sh "$CONFIG_PATH" "$FULL_EDERA_FLAVOR_CONFIG" "$DELTA_EDERA_FLAVOR_CONFIG" + echo "trimmed flavor delta config saved to $DELTA_EDERA_FLAVOR_CONFIG" diff --git a/hack/build/generate-clean-variant-config.sh b/hack/build/generate-clean-variant-config.sh index 91dc9aca..aca3e837 100755 --- a/hack/build/generate-clean-variant-config.sh +++ b/hack/build/generate-clean-variant-config.sh @@ -9,13 +9,13 @@ outputting a delta variant config containing only the changed/added lines from t Arguments: The edera flavor to start from ("zone" or "host") - The edera flavor kernel version to base this off of (recommend oldest-supported kver) + The published image tag to base this off of (a version like 6.18.52, or a branch tag like 6.18-lts) Target OCI image architecture (amd64, arm64) Path to the customized, variant kernel config file to compare against. May be complete, or a fragment. Path where the delta config containing only modified options will be saved. Example: - $(basename "$0") zone 5.4.293 amd64 /boot/myflavorvariant.config zone-myflavorvariant.config + $(basename "$0") zone 6.18-lts amd64 /boot/myflavorvariant.config zone-myflavorvariant.config Notes: - The script will fetch the latest released flavor config for the specified flavor from ghcr.io/edera-dev diff --git a/hack/build/generate-docker-script.py b/hack/build/generate-docker-script.py index d6f9c5a6..5942407e 100644 --- a/hack/build/generate-docker-script.py +++ b/hack/build/generate-docker-script.py @@ -132,6 +132,7 @@ def docker_compile( archs: list[str], firmware_url: str, firmware_sig_url: str, + source: dict[str, str], ) -> list[str]: """Generate docker run commands to compile the kernel with sccache.""" lines = [] @@ -180,7 +181,15 @@ def docker_compile( "-e", quoted("KERNEL_FLAVOR=%s" % flavor), "-e", - quoted("KERNEL_SRC_URL=/build/override-kernel-src.tar.xz"), + quoted("KERNEL_SRC_URL=/build/override-kernel-src.tar.gz"), + # Recorded in the image metadata so a published kernel names the + # exact tree it was built from; the SBOM reads it back from there. + "-e", + quoted("KERNEL_SRC_REPO=%s" % source.get("repo", "")), + "-e", + quoted("KERNEL_SRC_REF=%s" % source.get("ref", "")), + "-e", + quoted("KERNEL_SRC_COMMIT=%s" % source.get("commit", "")), # The Azure sccache env is passed through by name only (no values), # so the generated script stays free of secrets; docker omits any # that are unset on the host. Without Azure config sccache falls @@ -413,6 +422,7 @@ def generate_builds( kernel_archs: list[str], firmware_url: str, firmware_sig_url: str, + source: dict[str, str], tag_suffix: Optional[str] = None, ) -> list[str]: lines = [] @@ -437,6 +447,7 @@ def generate_builds( archs=kernel_archs, firmware_url=firmware_url, firmware_sig_url=firmware_sig_url, + source=source, ) # Phase 3: Package kernel and SDK images from the compiled artifacts. @@ -482,6 +493,11 @@ def generate_build_from_env() -> list[str]: root_firmware_url = os.getenv("FIRMWARE_URL") root_firmware_sig_url = os.getenv("FIRMWARE_SIG_URL") root_kernel_tags = os.getenv("KERNEL_TAGS", "").split(",") + root_source = { + "repo": os.getenv("KERNEL_SRC_REPO", ""), + "ref": os.getenv("KERNEL_SRC_REF", ""), + "commit": os.getenv("KERNEL_SRC_COMMIT", ""), + } archs_env = os.getenv("KERNEL_ARCHITECTURES", "") arch_env = os.getenv("KERNEL_ARCH", "") @@ -503,6 +519,7 @@ def generate_build_from_env() -> list[str]: kernel_archs=root_kernel_archs, firmware_url=root_firmware_url, firmware_sig_url=root_firmware_sig_url, + source=root_source, tag_suffix=get_branch_tag_suffix(), ) @@ -529,6 +546,11 @@ def generate_builds_from_matrix(matrix) -> list[str]: kernel_archs=build_archs, firmware_url=firmware_url, firmware_sig_url=firmware_sig_url, + source={ + "repo": build["repo"], + "ref": build["ref"], + "commit": build["commit"], + }, tag_suffix=tag_suffix, ) return lines diff --git a/hack/build/generate-matrix.py b/hack/build/generate-matrix.py index 38ad79e9..36b7e673 100644 --- a/hack/build/generate-matrix.py +++ b/hack/build/generate-matrix.py @@ -2,38 +2,22 @@ import sys import matrix -from util import parse_text_constraint, maybe, get_branch_tag_suffix -from packaging.version import Version, parse - - -def construct_stable_matrix(): - stable_matrix = matrix.generate_stable_matrix() - return stable_matrix - - -def construct_lts_matrix(): - lts_matrix = matrix.generate_lts_matrix() - return lts_matrix - - -def construct_backbuild_matrix(): - backbuild_matrix = matrix.generate_backbuild_matrix() - return backbuild_matrix - - -def construct_all_matrix(): - stable_matrix = construct_stable_matrix() - backbuild_matrix = construct_backbuild_matrix() - all_matrix = matrix.merge_matrix([stable_matrix, backbuild_matrix]) - return all_matrix - - -def construct_manual_matrix(exact_versions): - return matrix.generate_matrix(matrix.build_release_tags(exact_versions)) - - +from util import get_branch_tag_suffix, parse_text_constraint + +# A build spec is "" or ":", where constraints are +# semicolon-separated key=value pairs over `branch`, `flavor` and `arch` +# (e.g. "rebuild:branch=mainline;flavor=zone,host"). +# +# new - build only what the registry does not already have. Because every +# build carries an immutable -g tag, this means +# "build each branch that has moved since it was last built". +# rebuild - build everything the config selects, published or not. Use this +# when something other than the kernel source changed: a kconfig +# fragment, the buildenv, the packaging. DEFAULT_BUILD_SPEC = "new" +BUILD_SPEC_TYPES = ["new", "rebuild"] + if len(sys.argv) > 1: build_spec = sys.argv[1] else: @@ -48,71 +32,38 @@ def construct_manual_matrix(exact_versions): else: build_spec_data = "" -constraint = {} -if len(build_spec_data) > 0: - constraint = parse_text_constraint(build_spec_data) - -apply_config_versions = True - -if build_spec_type == "new": - first_matrix = matrix.filter_new_builds(construct_all_matrix()) -elif build_spec_type == "rebuild": - first_matrix = construct_all_matrix() -elif build_spec_type == "unsafe-all": - first_matrix = construct_all_matrix() - apply_config_versions = False -elif build_spec_type == "stable": - first_matrix = construct_stable_matrix() -elif build_spec_type == "lts": - first_matrix = construct_lts_matrix() -elif build_spec_type == "only-latest-lts": - first_matrix = construct_lts_matrix() - apply_config_versions = False - matrix.sort_matrix(first_matrix) - last_version = parse(first_matrix[-1]["version"]).base_version - last_version_builds = list( - filter(lambda build: last_version in build["version"], first_matrix) +if build_spec_type not in BUILD_SPEC_TYPES: + raise Exception( + "unknown build spec type: %s (expected one of %s)" + % (build_spec_type, ", ".join(BUILD_SPEC_TYPES)) ) - first_matrix = last_version_builds -elif build_spec_type == "only-latest": - first_matrix = construct_stable_matrix() - apply_config_versions = False - matrix.sort_matrix(first_matrix) - last_version = parse(first_matrix[-1]["version"]).base_version - last_version_builds = list( - filter(lambda build: last_version in build["version"], first_matrix) - ) - first_matrix = last_version_builds -elif build_spec_type == "override": - first_matrix = construct_all_matrix() - apply_config_versions = False -elif build_spec_type == "manual": - versions = maybe(constraint, "exact") - if versions is None: - versions = [] - first_matrix = construct_manual_matrix(versions) - apply_config_versions = False -else: - raise Exception("unknown build spec type: %s" % build_spec_type) - -if apply_config_versions: - final_matrix = matrix.filter_config_versions(first_matrix) -else: - final_matrix = first_matrix -if len(build_spec_data) > 0: - constraint = parse_text_constraint(build_spec_data) - final_matrix = matrix.filter_matrix(final_matrix, constraint) - -matrix.validate_produce_conflicts(final_matrix) -matrix.fill_runners(final_matrix) +final_matrix = matrix.generate_full_matrix() +# Builds from a branch of *this* repo (not of the kernel tree) publish under +# suffixed tags so they cannot overwrite the canonical ones. Applied before the +# `new` filter so that filter asks the registry about the tags this run would +# actually push, rather than about the canonical ones it will never touch. branch_suffix = get_branch_tag_suffix() if branch_suffix: for build in final_matrix: build["tags"] = ["%s-%s" % (t, branch_suffix) for t in build["tags"]] build["produces"] = ["%s-%s" % (p, branch_suffix) for p in build["produces"]] +if len(build_spec_data) > 0: + # Filter before consulting the registry so `new` only spends crane calls on + # the images the spec actually asked about. + final_matrix = matrix.filter_matrix( + final_matrix, parse_text_constraint(build_spec_data) + ) + +if build_spec_type == "new": + final_matrix = matrix.filter_new_builds(final_matrix) + +matrix.validate_produce_conflicts(final_matrix) +matrix.fill_runners(final_matrix) +matrix.sort_matrix(final_matrix) + merges = matrix.generate_merges(final_matrix) print("generated %s builds, %s merges" % (len(final_matrix), len(merges))) diff --git a/hack/build/generate-sbom.py b/hack/build/generate-sbom.py index cfe9b5c7..f5df10ef 100644 --- a/hack/build/generate-sbom.py +++ b/hack/build/generate-sbom.py @@ -6,21 +6,24 @@ container yields hundreds of toolchain/base-OS packages that have no bearing on the kernel that ships. Instead we describe what actually defines the kernel: - - the upstream linux source version it was built from, - - the patches applied to that source (the authoritative, arch-union list from - patchlist.py), and + - the exact commit of the Edera Linux tree it was built from (there is no + separate patch series: every Edera change is a commit on that branch, so + the commit is the complete statement of what is in this kernel), and - for GPU flavors, the firmware / nvidia module versions baked in. -All of this is architecture-independent -- the same source, patches, and module -versions apply to every arch in the manifest -- so a single SBOM correctly -describes the whole multi-arch image and stays correct as new arches (e.g. -arm64) are added. There are deliberately NO per-arch / package filter rules. +All of this is architecture-independent -- the same source and module versions +apply to every arch in the manifest -- so a single SBOM correctly describes the +whole multi-arch image and stays correct as new arches (e.g. arm64) are added. +There are deliberately NO per-arch / package filter rules. Reads from the environment (set by the merge job): - KERNEL_VERSION e.g. "6.18.35" or "6.18.35+nvidia-610.43.02" - KERNEL_FLAVOR e.g. "zone", "host", "zone-amdgpu", "zone-nvidiagpu" - KERNEL_SRC_URL upstream linux source tarball URL - FIRMWARE_URL linux-firmware tarball URL (only used for zone-amdgpu) + KERNEL_VERSION e.g. "6.18.52" or "6.18.52+nvidia-610.43.02" + KERNEL_FLAVOR e.g. "zone", "host", "zone-amdgpu", "zone-nvidiagpu" + KERNEL_SRC_URL source archive URL for the commit that was built + KERNEL_SRC_REPO Edera Linux repository URL + KERNEL_SRC_REF branch built, e.g. "edera/6.18-lts" + KERNEL_SRC_COMMIT commit built + FIRMWARE_URL linux-firmware tarball URL (only used for zone-amdgpu) Writes sbom.cdx.json (CycloneDX 1.6) in the current directory. @@ -30,25 +33,6 @@ import json import os import re -import subprocess -import sys - - -def applied_patches(version, flavor): - """The patch files applied to this (version, flavor). - - Delegates to patchlist.py so the SBOM lists exactly the patches the build - applies. patchlist.py matches constraints WITHOUT an arch argument, so this - is the union across architectures -- correct for a manifest-level SBOM and - forward-compatible with future arch-specific patches. - """ - out = subprocess.run( - [sys.executable, "hack/build/patchlist.py", version, flavor], - check=True, - stdout=subprocess.PIPE, - text=True, - ).stdout - return [line.strip() for line in out.splitlines() if line.strip()] def firmware_version_from_url(url): @@ -61,6 +45,9 @@ def main(): version = os.environ["KERNEL_VERSION"] flavor = os.environ["KERNEL_FLAVOR"] src_url = os.environ.get("KERNEL_SRC_URL", "") + src_repo = os.environ.get("KERNEL_SRC_REPO", "") + src_ref = os.environ.get("KERNEL_SRC_REF", "") + src_commit = os.environ.get("KERNEL_SRC_COMMIT", "") firmware_url = os.environ.get("FIRMWARE_URL", "") # Strip any "+nvidia-" local suffix to get the upstream kernel version. @@ -74,18 +61,22 @@ def main(): "version": kernel_version, "purl": kernel_ref, } + external_references = [] if src_url: - linux_component["externalReferences"] = [ - {"type": "distribution", "url": src_url} - ] - - patches = applied_patches(version, flavor) - if patches: - linux_component["pedigree"] = { - "patches": [ - {"type": "unofficial", "diff": {"url": patch}} for patch in patches - ] - } + external_references.append({"type": "distribution", "url": src_url}) + if src_repo: + external_references.append({"type": "vcs", "url": src_repo}) + if external_references: + linux_component["externalReferences"] = external_references + + # The downstream commit is the pedigree. CycloneDX models that as a commit + # ancestry rather than a patch list, which is the honest shape here: there + # are no out-of-tree diffs to enumerate, just the branch this was cut from. + if src_commit: + commit_entry = {"uid": src_commit} + if src_repo: + commit_entry["url"] = "%s/commit/%s" % (src_repo.rstrip("/"), src_commit) + linux_component["pedigree"] = {"commits": [commit_entry]} components = [linux_component] depends_on = [kernel_ref] @@ -142,6 +133,9 @@ def main(): }, "properties": [ {"name": "dev.edera.kernel.flavor", "value": flavor}, + {"name": "dev.edera.kernel.source.repo", "value": src_repo}, + {"name": "dev.edera.kernel.source.ref", "value": src_ref}, + {"name": "dev.edera.kernel.source.commit", "value": src_commit}, ], }, "components": components, @@ -159,7 +153,7 @@ def main(): "flavor": flavor, "version": version, "kernel": kernel_version, - "patches": len(patches), + "source": "%s@%s" % (src_ref, src_commit), "components": len(components), }, indent=2, diff --git a/hack/build/matrix.py b/hack/build/matrix.py index 8cdeecb5..427bea12 100644 --- a/hack/build/matrix.py +++ b/hack/build/matrix.py @@ -1,16 +1,24 @@ -import json import os +import random import re - -import yaml import subprocess +import sys +import time +import urllib.error import urllib.request from collections import OrderedDict from functools import cache +import yaml from packaging.version import Version, parse -from util import matches_constraints, list_remote_git_tags, format_image_name +from util import ( + format_image_name, + list_remote_git_tags, + matches_constraints, + maybe, + resolve_remote_branch, +) try: from yaml import CLoader as Loader @@ -22,6 +30,164 @@ image_name_format = CONFIG["imageNameFormat"] +KERNEL_CDN = "https://cdn.kernel.org/pub/linux/kernel" + +GITHUB_PREFIX = "https://github.com/" + +# git abbreviates to 12 characters for a repository the size of linux.git, and +# the kernel's own scripts/setlocalversion does the same. Shorter prefixes are +# not safely unique across the millions of objects in that history, and an +# image tag that silently aliases two commits is worse than a long tag. +SHORT_COMMIT_LENGTH = 12 + + +@cache +def source_repo() -> str: + return CONFIG["source"]["repo"].rstrip("/") + + +@cache +def _github_slug() -> str: + """The `owner/name` of the configured source repo. + + Resolving a branch to a commit only needs git, but reading one file out of + that commit (the Makefile, for the kernel version) and fetching the source + archive both go through GitHub's HTTP endpoints. Moving to a different + forge means teaching these two URL builders about it, which is why the + assumption fails loudly here rather than 404ing later. + """ + repo = source_repo() + if not repo.startswith(GITHUB_PREFIX): + raise Exception( + "source.repo must be a https://github.com/ URL, got %s " + "(archive and raw-file URLs are GitHub-specific)" % repo + ) + slug = repo[len(GITHUB_PREFIX) :].strip("/") + if slug.endswith(".git"): + slug = slug[: -len(".git")] + if slug.count("/") != 1: + raise Exception("source.repo is not an owner/name GitHub URL: %s" % repo) + return slug + + +def source_archive_url(commit: str) -> str: + """Tarball of the tree at `commit`. + + Addressed by commit rather than by branch so the URL is immutable: buildkit + caches the `ADD` by URL, and a branch-addressed URL would let a stale cache + entry silently serve the wrong source. The archive carries no .git + directory, so scripts/setlocalversion contributes nothing and `uname -r` + stays the plain kernel version, exactly as it did with kernel.org tarballs. + """ + return "%s%s/archive/%s.tar.gz" % (GITHUB_PREFIX, _github_slug(), commit) + + +def source_raw_url(commit: str, path: str) -> str: + return "https://raw.githubusercontent.com/%s/%s/%s" % ( + _github_slug(), + commit, + path, + ) + + +def fetch_url_text(url: str, attempts: int = 6) -> str: + for attempt in range(attempts): + try: + with urllib.request.urlopen(url, timeout=60) as response: + return response.read().decode("utf-8") + except (urllib.error.URLError, TimeoutError) as error: + sys.stderr.write( + "fetching %s failed (attempt %d/%d): %s\n" + % (url, attempt + 1, attempts, error) + ) + if attempt + 1 >= attempts: + raise + time.sleep(min(120, 10 * 2**attempt) + random.uniform(0, 5)) + + +MAKEFILE_VERSION_FIELD = re.compile( + r"^(VERSION|PATCHLEVEL|SUBLEVEL|EXTRAVERSION)\s*=\s*(.*?)\s*$" +) + + +def kernel_version_from_makefile(makefile: str) -> str: + """The kernel version a Makefile declares, e.g. "6.18.52" or "7.3.0-rc3". + + The Edera branches carry no release tags of their own, so the Makefile is + the authority on what version a branch currently is. These are the same + four fields the kernel's own build uses to form KERNELRELEASE, so what we + tag an image with is what the kernel inside it reports. + """ + fields = {} + for line in makefile.splitlines(): + match = MAKEFILE_VERSION_FIELD.match(line) + if match and match.group(1) not in fields: + fields[match.group(1)] = match.group(2) + for required in ("VERSION", "PATCHLEVEL"): + if not fields.get(required): + raise Exception("kernel Makefile has no %s" % required) + version = "%s.%s.%s" % ( + fields["VERSION"], + fields["PATCHLEVEL"], + fields.get("SUBLEVEL") or "0", + ) + # "-rc3" and friends attach directly, matching how the kernel spells it. + return version + fields.get("EXTRAVERSION", "") + + +@cache +def resolve_branches() -> tuple[dict[str, any], ...]: + """Resolve every configured branch to a commit and a kernel version. + + Everything downstream keys off this: the commit fixes the source archive + and the immutable image tag, the version fixes the moving tags. + """ + repo = source_repo() + resolved = [] + for branch_info in CONFIG["branches"]: + name = branch_info["name"] + ref = branch_info["ref"] + commit = resolve_remote_branch(repo, ref) + version = kernel_version_from_makefile( + fetch_url_text(source_raw_url(commit, "Makefile")) + ) + resolved.append( + { + "name": name, + "ref": ref, + "repo": repo, + "commit": commit, + "short_commit": commit[:SHORT_COMMIT_LENGTH], + "version": version, + "aliases": list(maybe(branch_info, "aliases", [])), + } + ) + return tuple(resolved) + + +def branch_tags(branch: dict[str, any]) -> list[str]: + """Every tag a build of this branch publishes, immutable one first. + + The `-g` tag is unique to one commit and is never reused, + which is both what makes a rebuild detectable (see filter_new_builds) and + what lets a consumer pin to an exact tree. Everything after it moves. + """ + version = branch["version"] + version_info = parse(version) + tags = ["%s-g%s" % (version, branch["short_commit"]), version] + # A prerelease must not claim the series tag: `7.3` belongs to 7.3 proper, + # not to the 7.3-rc3 that precedes it. + if not version_info.is_prerelease: + tags.append("%s.%s" % (version_info.major, version_info.minor)) + tags.append(branch["name"]) + tags += branch["aliases"] + + unique = [] + for tag in tags: + if tag not in unique: + unique.append(tag) + return unique + @cache def default_architectures() -> list[str]: @@ -40,36 +206,12 @@ def flavor_architectures(flavor_info: dict[str, any]) -> list[str]: return default_architectures() -@cache -def get_current_kernel_releases() -> dict[str, any]: - with urllib.request.urlopen("https://www.kernel.org/releases.json") as response: - releases = json.load(response) - return releases - - -@cache -def get_all_kernel_releases() -> list[str]: - # Release tags (vX.Y[.Z]) map 1:1 to the published linux-X.Y[.Z].tar.xz - # artifacts; "-" excludes -rc and other pre-release tags. Tags only reach - # back to v2.6.11, but nothing anywhere near that old is buildable here. - releases = [] - for tag in list_remote_git_tags( - "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git" - ): - if not tag.startswith("v"): - continue - kernel_version = tag[1:] - if "-" in kernel_version: - continue - releases.append(kernel_version) - return releases - - @cache def get_all_firmware_releases() -> list[str]: # Snapshot tags are pure YYYYMMDD and map 1:1 to the published # linux-firmware-YYYYMMDD.tar.xz artifacts, so lexicographic sort is - # chronological. + # chronological. This is the one remaining kernel.org dependency, and it + # has nothing to do with the kernel source. snapshots = [] for tag in list_remote_git_tags( "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git" @@ -82,23 +224,6 @@ def get_all_firmware_releases() -> list[str]: return snapshots -def merge_matrix(matrix_list: list[list[dict[str, any]]]) -> list[dict[str, any]]: - all_builds = OrderedDict() # type: dict[str, dict[str, any]] - for builds in matrix_list: - for item in builds: - key = "%s::%s::%s" % (item["version"], item["flavor"], item["arch"]) - if key not in all_builds: - all_builds[key] = item - else: - for tag in item["tags"]: - if tag not in all_builds[key]["tags"]: - all_builds[key]["tags"].append(tag) - - builds = list(all_builds.values()) - builds.sort(key=lambda build: parse(build["version"])) - return builds - - def extract_base_images(builds: list[dict[str, any]]): images = [] for build in builds: @@ -127,14 +252,16 @@ def find_existing_tags(images: list[str]) -> dict[str, list[str]]: def validate_produce_conflicts(builds: list[dict[str, any]]): - # produces are intentionally shared across arches for the same (version, flavor); - # each arch build pushes its single-platform image by digest, and the merge step - # later tags the combined manifest list. Only flag when the *same* image:tag is - # claimed by two different (version, flavor) combinations. + # produces are intentionally shared across arches for the same + # (branch, flavor); each arch build pushes its single-platform image by + # digest, and the merge step later tags the combined manifest list. Only + # flag when the *same* image:tag is claimed by two different + # (branch, flavor) combinations -- most plausibly two branches that have + # converged on one version, or that both claim an alias like `latest`. produce_owner = {} for build in builds: - owner = "%s::%s" % (build["version"], build["flavor"]) + owner = "%s::%s" % (build["branch"], build["flavor"]) for produce in build["produces"]: if produce in produce_owner and produce_owner[produce] != owner: raise Exception( @@ -145,6 +272,13 @@ def validate_produce_conflicts(builds: list[dict[str, any]]): def filter_new_builds(builds: list[dict[str, any]]) -> list[dict[str, any]]: + """Drop builds whose every published tag is already in the registry. + + Because each build's tag list leads with `-g`, a branch + that has moved always looks new here even when its kernel version has not + changed -- the normal case for a downstream branch picking up a patch + between upstream releases. + """ images = extract_base_images(builds) existing = find_existing_tags(images) should_builds = [] @@ -163,78 +297,24 @@ def filter_new_builds(builds: list[dict[str, any]]) -> list[dict[str, any]]: return should_builds -def limit_gh_builds(builds: list[dict[str, any]]) -> list[dict[str, any]]: - builds.sort(key=lambda build: parse(build["version"])) - - if len(builds) > 250: - builds = builds[-250:] - return builds - - -def is_release_current(version: str) -> bool: - current_kernel_releases = get_current_kernel_releases() - latest_stable = current_kernel_releases["latest_stable"]["version"] - is_current = False - if latest_stable is not None and version == latest_stable: - is_current = True - return is_current - for release in current_kernel_releases["releases"]: - if not release["moniker"] in ["stable", "longterm"]: - continue - if release["version"] == version: - is_current = True - break - return is_current - - def filter_matrix( builds: list[dict[str, any]], constraint: dict[str, any] ) -> list[dict[str, any]]: output_builds = [] for build in builds: - version = build["version"] - version_info = parse(version) - flavor = build["flavor"] - is_current_release = is_release_current(version_info.base_version) - should_build = matches_constraints( - version_info, - flavor, + if matches_constraints( + build["branch"], + build["flavor"], constraint, - is_current_release=is_current_release, arch=build.get("arch"), - ) - if should_build: - output_builds.append(build) - return output_builds - - -def filter_config_versions(builds: list[dict[str, any]]) -> list[dict[str, any]]: - output_builds = [] - for build in builds: - version = build["version"] - version_info = parse(version) - flavor = build["flavor"] - is_current_release = is_release_current(version_info.base_version) - should_build = False - for constraint in CONFIG["versions"]: - if matches_constraints( - version_info, flavor, constraint, is_current_release=is_current_release - ): - should_build = True - if should_build: + ): output_builds.append(build) - return output_builds -def generate_matrix(tags: dict[str, str]) -> list[dict[str, any]]: - unique_versions = list(set(tags.values())) - unique_versions.sort(key=Version) - +def generate_matrix(branches: list[dict[str, any]]) -> list[dict[str, any]]: version_builds = [] - kernel_cdn = "https://cdn.kernel.org/pub/linux/kernel" - # TODO later on we could get cute and let the config drive # which firmware snapshot to use - but as far as the official firmware goes # latest should be fine/preferred. @@ -243,99 +323,74 @@ def generate_matrix(tags: dict[str, str]) -> list[dict[str, any]]: latest_firmware = all_firmware_releases[0] firmware_url = "%s/firmware/linux-firmware-%s.tar.xz" % ( - kernel_cdn, + KERNEL_CDN, latest_firmware, ) firmware_sig_url = "%s/firmware/linux-firmware-%s.tar.sign" % ( - kernel_cdn, + KERNEL_CDN, latest_firmware, ) - for version in unique_versions: - version_tags = [] - for tag in tags: - tag_version = tags[tag] - if tag_version == version: - version_tags.append(tag) + for branch in branches: + version = branch["version"] version_info = parse(version) + src_url = source_archive_url(branch["commit"]) + base_tags = branch_tags(branch) - version_for_url = version - if version_info.micro == 0: - version_for_url = "%s.%s" % (version_info.major, version_info.minor) - - src_url = "%s/v%s.x/linux-%s.tar.xz" % ( - kernel_cdn, - version_info.major, - version_for_url, - ) for flavor_info in CONFIG["flavors"]: flavor = flavor_info["name"] if "constraints" in flavor_info and not matches_constraints( - version_info, flavor, flavor_info["constraints"] + branch["name"], flavor, flavor_info["constraints"] ): continue architectures = flavor_architectures(flavor_info) - if "local_tags" in flavor_info: - for local_tag in flavor_info["local_tags"]: - produces = [] - local_version_tags = [] - for tag in version_tags: - local_append = tag + "-" + local_tag - local_version_tags.append(local_append) - kernel_output = format_image_name( + # A flavor with local_tags publishes one distinct image per tag + # (today: one per NVIDIA driver series), each carrying the whole + # tag set with the local tag appended. + local_tags = maybe(flavor_info, "local_tags", [None]) + for local_tag in local_tags: + if local_tag is None: + build_version = version + tags = list(base_tags) + else: + build_version = "%s+%s" % (version, local_tag) + tags = ["%s-%s" % (tag, local_tag) for tag in base_tags] + + produces = [] + for tag in tags: + produces.append( + format_image_name( image_name_format, flavor, version_info, "[flavor]-kernel", - local_append, + tag, ) - kernel_sdk_output = format_image_name( + ) + produces.append( + format_image_name( image_name_format, flavor, version_info, "[flavor]-kernel-sdk", - local_append, - ) - produces.append(kernel_output) - produces.append(kernel_sdk_output) - for arch in architectures: - version_builds.append( - { - "version": version + "+" + local_tag, - "firmware_url": firmware_url, - "firmware_sig_url": firmware_sig_url, - "tags": local_version_tags, - "source": src_url, - "flavor": flavor, - "arch": arch, - "produces": produces, - } + tag, ) - else: - produces = [] - for tag in version_tags: - kernel_output = format_image_name( - image_name_format, flavor, version_info, "[flavor]-kernel", tag ) - kernel_sdk_output = format_image_name( - image_name_format, - flavor, - version_info, - "[flavor]-kernel-sdk", - tag, - ) - produces.append(kernel_output) - produces.append(kernel_sdk_output) + for arch in architectures: version_builds.append( { - "version": version, + "branch": branch["name"], + "ref": branch["ref"], + "repo": branch["repo"], + "commit": branch["commit"], + "version": build_version, "firmware_url": firmware_url, "firmware_sig_url": firmware_sig_url, - "tags": version_tags, + "tags": tags, "source": src_url, "flavor": flavor, "arch": arch, @@ -345,6 +400,10 @@ def generate_matrix(tags: dict[str, str]) -> list[dict[str, any]]: return version_builds +def generate_full_matrix() -> list[dict[str, any]]: + return generate_matrix(list(resolve_branches())) + + def summarize_matrix(builds: list[dict[str, any]]): for build in builds: tags = [] @@ -358,10 +417,12 @@ def summarize_matrix(builds: list[dict[str, any]]): image_names.append(image_name) tags.sort() print( - "build %s %s for %s with tags %s to %s on %s" + "build %s %s (%s @ %s) for %s with tags %s to %s on %s" % ( build["flavor"], build["version"], + build["branch"], + build["commit"][:SHORT_COMMIT_LENGTH], build["arch"], ", ".join(tags), ", ".join(image_names), @@ -370,102 +431,13 @@ def summarize_matrix(builds: list[dict[str, any]]): ) -def build_release_tags(versions: list[str]) -> dict[str, str]: - tags = {} - for raw_version in versions: - parsed_ver = parse(raw_version) - # Hardcode skip of pre-5.x.x kernels - if parsed_ver.major < 5: - print(f"skipping {raw_version}, too old to support") - continue - major = str(parsed_ver.major) - major_minor = "%s.%s" % (parsed_ver.major, parsed_ver.minor) - if major not in tags or parse(tags[major]) < parsed_ver: - tags[major] = raw_version - if major_minor not in tags or parse(tags[major_minor]) < parsed_ver: - tags[major_minor] = raw_version - for tag in list(tags.keys()): - tags[tags[tag]] = tags[tag] - return tags - - -def generate_stable_matrix() -> list[dict[str, any]]: - current_kernel_releases = get_current_kernel_releases() - latest_stable = current_kernel_releases["latest_stable"]["version"] - versions = [ - r["version"] - for r in current_kernel_releases["releases"] - if r["moniker"] in ["stable", "longterm"] - ] - tags = build_release_tags(versions) - tags["stable"] = latest_stable - tags["latest"] = latest_stable - return generate_matrix(tags) - - -def generate_lts_matrix() -> list[dict[str, any]]: - current_kernel_releases = get_current_kernel_releases() - versions = [ - r["version"] - for r in current_kernel_releases["releases"] - if r["moniker"] == "longterm" - ] - return generate_matrix(build_release_tags(versions)) - - -def generate_backbuild_matrix() -> list[dict[str, any]]: - tags = {} - major_minors = {} - - all_releases = get_all_kernel_releases() - for version in all_releases: - parts = parse(version) - major_minor = "%s.%s" % (parts.major, parts.minor) - - if major_minor in tags: - existing = tags[major_minor] - if parse(existing) < parts: - tags[major_minor] = version - major_minors[major_minor] = version - else: - tags[major_minor] = version - major_minors[major_minor] = version - - for tag in list(tags.keys()): - version = tags[tag] - tags[version] = version - - current_kernel_releases = get_current_kernel_releases() - for release in current_kernel_releases["releases"]: - if not release["moniker"] in ["stable", "longterm"]: - continue - for key in list(tags.keys()): - if tags[key] == release["version"]: - tags.pop(key) - for key in list(major_minors.keys()): - if major_minors[key] == release["version"]: - major_minors.pop(key) - parts = parse(release["version"]) - major_minor = "%s.%s" % (parts.major, parts.minor) - if major_minor in major_minors: - major_minors.pop(major_minor) - if major_minor in tags: - tags.pop(major_minor) - return generate_matrix(tags) - - def pick_runner(build: dict[str, any]) -> str: - version: str = build["version"] - version_info: Version = parse(version) - flavor: str = build["flavor"] - arch: str = build["arch"] for runner in CONFIG["runners"]: if matches_constraints( - version_info, - flavor, + build["branch"], + build["flavor"], runner, - is_current_release=is_release_current(version_info.base_version), - arch=arch, + arch=build["arch"], ): return runner["name"] raise Exception("No runner found for build %s" % build) @@ -477,28 +449,34 @@ def fill_runners(builds: list[dict[str, any]]): def sort_matrix(builds: list[dict[str, any]]): - builds.sort(key=lambda build: Version(build["version"])) + builds.sort( + key=lambda build: (Version(build["version"]), build["flavor"], build["arch"]) + ) def generate_merges(builds: list[dict[str, any]]) -> list[dict[str, any]]: - """Group per-arch builds into one merge entry per (version, flavor). + """Group per-arch builds into one merge entry per (branch, version, flavor). - The merge job runs after all per-arch build jobs for that (version, flavor) - complete; it stitches the single-platform pushes into a manifest list per - produced image:tag. + The merge job runs after all per-arch build jobs for that group complete; + it stitches the single-platform pushes into a manifest list per produced + image:tag. """ merges = OrderedDict() # type: dict[str, dict[str, any]] for build in builds: - key = "%s::%s" % (build["version"], build["flavor"]) + key = "%s::%s::%s" % (build["branch"], build["version"], build["flavor"]) if key not in merges: merges[key] = { + "branch": build["branch"], "version": build["version"], "flavor": build["flavor"], "tags": list(build["tags"]), "produces": list(build["produces"]), "archs": [build["arch"]], # Carried for SBOM generation in the merge job; identical across - # archs for a given (version, flavor). + # archs for a given (branch, flavor). + "repo": build["repo"], + "ref": build["ref"], + "commit": build["commit"], "source": build["source"], "firmware_url": build["firmware_url"], } diff --git a/hack/build/patchlist.py b/hack/build/patchlist.py deleted file mode 100644 index 98a16973..00000000 --- a/hack/build/patchlist.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys - -from packaging.version import parse - -from matrix import CONFIG -from util import matches_constraints, maybe - -if len(sys.argv) != 3: - print("Usage: patchlist ") - exit(1) - -try: - target_version = parse(sys.argv[1]) -except Exception: - target_version = parse(sys.argv[1].split("-")[0]) -kernel_flavor = sys.argv[2] -series = "%s.%s" % (target_version.major, target_version.minor) - - -patches = CONFIG["patches"] - -apply_patches = [] - - -for patch in patches: - if "patch" in patch: - file_names = [patch["patch"]] - else: - file_names = patch["patches"] - order = maybe(patch, "order") - - if order is None: - order = 1 - - apply = matches_constraints(target_version, kernel_flavor, patch) - - if apply: - for file_name in file_names: - apply_patches.append( - { - "patch": file_name, - "order": order, - } - ) - -apply_patches.sort(key=lambda p: p["order"]) - -for patch in apply_patches: - print("patches/%s" % patch["patch"]) diff --git a/hack/build/util.py b/hack/build/util.py index 2070558f..85652173 100644 --- a/hack/build/util.py +++ b/hack/build/util.py @@ -40,93 +40,62 @@ def maybe(m: dict[str, any], k: str, default_value: any = None) -> any: def matches_constraints( - version: Version, + branch: str, flavor: str, constraints: dict[str, any], - is_current_release=None, arch: Optional[str] = None, ) -> bool: + """Does a (branch, flavor, arch) build match a constraint block? + + Constraints are matched on names, not version ranges: the kernel version is + a property of whatever edera-dev/linux branch is being built, not something + this repo picks, so anything that wants to scope itself to a particular + kernel says which branch it means. + + Recognized keys: `branches`, `flavors`, `arch`, and `any` (a list of + constraint blocks, matching if any one of them does). A key that is absent + places no restriction; an empty constraint block matches everything. + """ if "any" in constraints: for constraint in constraints["any"]: - if matches_constraints( - version, - flavor, - constraint, - is_current_release=is_current_release, - arch=arch, - ): + if matches_constraints(branch, flavor, constraint, arch=arch): return True return False - major_minor_series = "%s.%s" % (version.major, version.minor) - major_series = str(version.major) - + branches = maybe(constraints, "branches") flavors = maybe(constraints, "flavors") - lower = maybe(constraints, "lower") - only_series = maybe(constraints, "series") - upper = maybe(constraints, "upper") - exact = maybe(constraints, "exact") - current = maybe(constraints, "current") arch_constraint = maybe(constraints, "arch") - if lower is not None: - lower = Version(lower) - if upper is not None: - upper = Version(upper) - if exact is str: - exact = [exact] - - applies = True - - if is_current_release is not None and current is not None: - if is_current_release != current: - applies = False - - if lower is None and upper is not None: - if version > upper: - applies = False - - if lower is not None and upper is None: - if version < lower: - applies = False + if type(branches) is str: + branches = [branches] + if type(flavors) is str: + flavors = [flavors] + if type(arch_constraint) is str: + arch_constraint = [arch_constraint] - if lower is not None and upper is not None: - if version < lower or version > upper: - applies = False - - if type(only_series) is str: - only_series = [only_series] - - if only_series is not None and ( - (major_minor_series not in only_series) and (major_series not in only_series) - ): - applies = False + if branches is not None and branch is not None and branch not in branches: + return False if flavors is not None and flavor not in flavors: - applies = False - - version_string = str(version) - if exact is not None and not version_string in exact: - applies = False + return False - if arch_constraint is not None and arch is not None: - if type(arch_constraint) is str: - arch_constraint = [arch_constraint] - if arch not in arch_constraint: - applies = False + if arch_constraint is not None and arch is not None and arch not in arch_constraint: + return False - return applies + return True -def list_remote_git_tags(url: str, attempts: int = 6) -> list[str]: - # ls-remote fetches only the tag advertisement (protocol v2 filters it - # server-side), so this avoids both a clone and rsync.kernel.org's - # aggressive concurrent-connection cap. Retries cover transient network - # failures, with stderr surfaced so the failure mode shows up in CI logs. +def _git_ls_remote( + args: list[str], url: str, patterns: list[str] = [], attempts: int = 6 +) -> bytes: + # ls-remote fetches only the ref advertisement (protocol v2 filters it + # server-side), so this avoids a clone entirely. Retries cover transient + # network failures, with stderr surfaced so the failure mode shows up in CI + # logs. for attempt in range(attempts): try: result = subprocess.run( - ["git", "ls-remote", "--tags", "--refs", url], + ["git", "ls-remote", *args, url, *patterns], stdout=subprocess.PIPE, stderr=subprocess.PIPE, # git has no network timeout of its own, so without this a @@ -136,7 +105,7 @@ def list_remote_git_tags(url: str, attempts: int = 6) -> list[str]: ) except subprocess.TimeoutExpired: sys.stderr.write( - "listing tags of %s timed out (attempt %d/%d)\n" + "listing refs of %s timed out (attempt %d/%d)\n" % (url, attempt + 1, attempts) ) if attempt + 1 < attempts: @@ -146,14 +115,19 @@ def list_remote_git_tags(url: str, attempts: int = 6) -> list[str]: if result.returncode == 0: break sys.stderr.write( - "listing tags of %s failed (attempt %d/%d):\n%s\n" + "listing refs of %s failed (attempt %d/%d):\n%s\n" % (url, attempt + 1, attempts, result.stderr.decode("utf-8", "replace")) ) if attempt + 1 < attempts: time.sleep(min(120, 10 * 2**attempt) + random.uniform(0, 5)) result.check_returncode() + return result.stdout + + +def list_remote_git_tags(url: str, attempts: int = 6) -> list[str]: + stdout = _git_ls_remote(["--tags", "--refs"], url, attempts=attempts) tags = [] - for line in result.stdout.splitlines(keepends=False): + for line in stdout.splitlines(keepends=False): # "\trefs/tags/" parts = line.decode("utf-8").strip().split("\t") if len(parts) != 2 or not parts[1].startswith("refs/tags/"): @@ -162,30 +136,48 @@ def list_remote_git_tags(url: str, attempts: int = 6) -> list[str]: return tags +def resolve_remote_branch(url: str, branch: str, attempts: int = 6) -> str: + """Resolve a branch name on a remote to the commit it currently points at. + + Matching is against refs/heads/ exactly: `--heads ` would + also match a ref whose name merely ends in the same path component, and + silently building the wrong branch is far worse than failing here. + """ + ref = "refs/heads/%s" % branch + stdout = _git_ls_remote(["--heads"], url, patterns=[ref], attempts=attempts) + for line in stdout.splitlines(keepends=False): + parts = line.decode("utf-8").strip().split("\t") + if len(parts) != 2: + continue + if parts[1] == ref: + return parts[0] + raise Exception("branch %s does not exist on %s" % (branch, url)) + + def parse_text_bool(text: str) -> bool: return text.lower() in ["1", "true", "yes"] def parse_text_constraint(text: str) -> dict[str, any]: + """Parse a build-spec constraint string, e.g. "branch=mainline;flavor=zone". + + Keys mirror the constraint blocks in config.yaml. `branch` and `flavor` are + accepted as singular spellings of `branches` and `flavors`; values are + comma-separated. + """ constraint = {} for item in text.split(";"): item = item.strip() + if not item: + continue parts = item.split("=", maxsplit=1) if len(parts) != 2: parts = [parts[0], ""] key = parts[0] value = parts[1] - if key == "current": - constraint[key] = parse_text_bool(value) - elif key == "lower" or key == "upper": - constraint[key] = value - elif ( - key == "flavors" - or key == "flavor" - or key == "series" - or key == "exact" - or key == "arch" - ): + if key in ["branch", "branches", "flavor", "flavors", "arch"]: + if key == "branch": + key = "branches" if key == "flavor": key = "flavors" constraint[key] = value.split(",") diff --git a/patches/0001-9p-xen-mark-9p-transport-device-as-closing-when-remo.patch b/patches/0001-9p-xen-mark-9p-transport-device-as-closing-when-remo.patch deleted file mode 100644 index f8870f4f..00000000 --- a/patches/0001-9p-xen-mark-9p-transport-device-as-closing-when-remo.patch +++ /dev/null @@ -1,35 +0,0 @@ -From 8e7d925b756b09c7c52b1b06a3c3c996d9127d64 Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Mon, 8 Dec 2025 11:40:56 -0800 -Subject: [PATCH] 9p/xen: mark 9p transport device as closing when removing it - -We need to do this so that we can signal to the other end that the -device is being removed, so that it will release its claim on the -underlying memory allocation. Otherwise releasing the grant-table -entries is deferred resulting in a kernel oops since the pages have -already been freed. - -Cc: Juergen Gross -Cc: Stefano Stabellini -Fixes: 71ebd71921e45 ("xen/9pfs: connect to the backend") -Signed-off-by: Ariadne Conill -Signed-off-by: Alex Zenla ---- - net/9p/trans_xen.c | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/net/9p/trans_xen.c b/net/9p/trans_xen.c -index b9ff69c7522a..cde283c42dc6 100644 ---- a/net/9p/trans_xen.c -+++ b/net/9p/trans_xen.c -@@ -312,6 +312,7 @@ static void xen_9pfs_front_remove(struct xenbus_device *dev) - { - struct xen_9pfs_front_priv *priv = dev_get_drvdata(&dev->dev); - -+ xenbus_switch_state(dev, XenbusStateClosing); - dev_set_drvdata(&dev->dev, NULL); - xen_9pfs_front_free(priv); - } --- -2.52.0 - diff --git a/patches/0001-Revert-ALSA-memalloc-Workaround-for-Xen-PV.patch b/patches/0001-Revert-ALSA-memalloc-Workaround-for-Xen-PV.patch deleted file mode 100644 index 15e1b78a..00000000 --- a/patches/0001-Revert-ALSA-memalloc-Workaround-for-Xen-PV.patch +++ /dev/null @@ -1,186 +0,0 @@ -From 8f231047c835c71e893c86f609f5430adcd3a92a Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Thu, 5 Sep 2024 16:05:18 -0700 -Subject: [PATCH] Revert "ALSA: memalloc: Workaround for Xen PV" - -This reverts commit 53466ebdec614f915c691809b0861acecb941e30. ---- - sound/core/memalloc.c | 87 +++++++++---------------------------------- - 1 file changed, 18 insertions(+), 69 deletions(-) - -diff --git a/sound/core/memalloc.c b/sound/core/memalloc.c -index f901504b5afc..81025f50a542 100644 ---- a/sound/core/memalloc.c -+++ b/sound/core/memalloc.c -@@ -541,15 +541,16 @@ static void *snd_dma_noncontig_alloc(struct snd_dma_buffer *dmab, size_t size) - struct sg_table *sgt; - void *p; - --#ifdef CONFIG_SND_DMA_SGBUF -- if (cpu_feature_enabled(X86_FEATURE_XENPV)) -- return snd_dma_sg_fallback_alloc(dmab, size); --#endif - sgt = dma_alloc_noncontiguous(dmab->dev.dev, size, dmab->dev.dir, - DEFAULT_GFP, 0); - #ifdef CONFIG_SND_DMA_SGBUF -- if (!sgt && !get_dma_ops(dmab->dev.dev)) -+ if (!sgt && !get_dma_ops(dmab->dev.dev)) { -+ if (dmab->dev.type == SNDRV_DMA_TYPE_DEV_WC_SG) -+ dmab->dev.type = SNDRV_DMA_TYPE_DEV_WC_SG_FALLBACK; -+ else -+ dmab->dev.type = SNDRV_DMA_TYPE_DEV_SG_FALLBACK; - return snd_dma_sg_fallback_alloc(dmab, size); -+ } - #endif - if (!sgt) - return NULL; -@@ -716,38 +717,19 @@ static const struct snd_malloc_ops snd_dma_sg_wc_ops = { - - /* Fallback SG-buffer allocations for x86 */ - struct snd_dma_sg_fallback { -- bool use_dma_alloc_coherent; - size_t count; - struct page **pages; -- /* DMA address array; the first page contains #pages in ~PAGE_MASK */ -- dma_addr_t *addrs; - }; - - static void __snd_dma_sg_fallback_free(struct snd_dma_buffer *dmab, - struct snd_dma_sg_fallback *sgbuf) - { -- size_t i, size; -- -- if (sgbuf->pages && sgbuf->addrs) { -- i = 0; -- while (i < sgbuf->count) { -- if (!sgbuf->pages[i] || !sgbuf->addrs[i]) -- break; -- size = sgbuf->addrs[i] & ~PAGE_MASK; -- if (WARN_ON(!size)) -- break; -- if (sgbuf->use_dma_alloc_coherent) -- dma_free_coherent(dmab->dev.dev, size << PAGE_SHIFT, -- page_address(sgbuf->pages[i]), -- sgbuf->addrs[i] & PAGE_MASK); -- else -- do_free_pages(page_address(sgbuf->pages[i]), -- size << PAGE_SHIFT, false); -- i += size; -- } -- } -+ bool wc = dmab->dev.type == SNDRV_DMA_TYPE_DEV_WC_SG_FALLBACK; -+ size_t i; -+ -+ for (i = 0; i < sgbuf->count && sgbuf->pages[i]; i++) -+ do_free_pages(page_address(sgbuf->pages[i]), PAGE_SIZE, wc); - kvfree(sgbuf->pages); -- kvfree(sgbuf->addrs); - kfree(sgbuf); - } - -@@ -756,36 +738,24 @@ static void *snd_dma_sg_fallback_alloc(struct snd_dma_buffer *dmab, size_t size) - struct snd_dma_sg_fallback *sgbuf; - struct page **pagep, *curp; - size_t chunk, npages; -- dma_addr_t *addrp; - dma_addr_t addr; - void *p; -- -- /* correct the type */ -- if (dmab->dev.type == SNDRV_DMA_TYPE_DEV_SG) -- dmab->dev.type = SNDRV_DMA_TYPE_DEV_SG_FALLBACK; -- else if (dmab->dev.type == SNDRV_DMA_TYPE_DEV_WC_SG) -- dmab->dev.type = SNDRV_DMA_TYPE_DEV_WC_SG_FALLBACK; -+ bool wc = dmab->dev.type == SNDRV_DMA_TYPE_DEV_WC_SG_FALLBACK; - - sgbuf = kzalloc(sizeof(*sgbuf), GFP_KERNEL); - if (!sgbuf) - return NULL; -- sgbuf->use_dma_alloc_coherent = cpu_feature_enabled(X86_FEATURE_XENPV); - size = PAGE_ALIGN(size); - sgbuf->count = size >> PAGE_SHIFT; - sgbuf->pages = kvcalloc(sgbuf->count, sizeof(*sgbuf->pages), GFP_KERNEL); -- sgbuf->addrs = kvcalloc(sgbuf->count, sizeof(*sgbuf->addrs), GFP_KERNEL); -- if (!sgbuf->pages || !sgbuf->addrs) -+ if (!sgbuf->pages) - goto error; - - pagep = sgbuf->pages; -- addrp = sgbuf->addrs; -- chunk = (PAGE_SIZE - 1) << PAGE_SHIFT; /* to fit in low bits in addrs */ -+ chunk = size; - while (size > 0) { - chunk = min(size, chunk); -- if (sgbuf->use_dma_alloc_coherent) -- p = dma_alloc_coherent(dmab->dev.dev, chunk, &addr, DEFAULT_GFP); -- else -- p = do_alloc_pages(dmab->dev.dev, chunk, &addr, false); -+ p = do_alloc_pages(dmab->dev.dev, chunk, &addr, wc); - if (!p) { - if (chunk <= PAGE_SIZE) - goto error; -@@ -797,25 +767,17 @@ static void *snd_dma_sg_fallback_alloc(struct snd_dma_buffer *dmab, size_t size) - size -= chunk; - /* fill pages */ - npages = chunk >> PAGE_SHIFT; -- *addrp = npages; /* store in lower bits */ - curp = virt_to_page(p); -- while (npages--) { -+ while (npages--) - *pagep++ = curp++; -- *addrp++ |= addr; -- addr += PAGE_SIZE; -- } - } - - p = vmap(sgbuf->pages, sgbuf->count, VM_MAP, PAGE_KERNEL); - if (!p) - goto error; -- -- if (dmab->dev.type == SNDRV_DMA_TYPE_DEV_WC_SG_FALLBACK) -- set_pages_array_wc(sgbuf->pages, sgbuf->count); -- - dmab->private_data = sgbuf; - /* store the first page address for convenience */ -- dmab->addr = sgbuf->addrs[0] & PAGE_MASK; -+ dmab->addr = snd_sgbuf_get_addr(dmab, 0); - return p; - - error: -@@ -825,23 +787,10 @@ static void *snd_dma_sg_fallback_alloc(struct snd_dma_buffer *dmab, size_t size) - - static void snd_dma_sg_fallback_free(struct snd_dma_buffer *dmab) - { -- struct snd_dma_sg_fallback *sgbuf = dmab->private_data; -- -- if (dmab->dev.type == SNDRV_DMA_TYPE_DEV_WC_SG_FALLBACK) -- set_pages_array_wb(sgbuf->pages, sgbuf->count); - vunmap(dmab->area); - __snd_dma_sg_fallback_free(dmab, dmab->private_data); - } - --static dma_addr_t snd_dma_sg_fallback_get_addr(struct snd_dma_buffer *dmab, -- size_t offset) --{ -- struct snd_dma_sg_fallback *sgbuf = dmab->private_data; -- size_t index = offset >> PAGE_SHIFT; -- -- return (sgbuf->addrs[index] & PAGE_MASK) | (offset & ~PAGE_MASK); --} -- - static int snd_dma_sg_fallback_mmap(struct snd_dma_buffer *dmab, - struct vm_area_struct *area) - { -@@ -856,8 +805,8 @@ static const struct snd_malloc_ops snd_dma_sg_fallback_ops = { - .alloc = snd_dma_sg_fallback_alloc, - .free = snd_dma_sg_fallback_free, - .mmap = snd_dma_sg_fallback_mmap, -- .get_addr = snd_dma_sg_fallback_get_addr, - /* reuse vmalloc helpers */ -+ .get_addr = snd_dma_vmalloc_get_addr, - .get_page = snd_dma_vmalloc_get_page, - .get_chunk_size = snd_dma_vmalloc_get_chunk_size, - }; --- -2.39.2 - diff --git a/patches/0001-drm-virtio-use-the-DMA-API-for-resource-backing-on-X.patch b/patches/0001-drm-virtio-use-the-DMA-API-for-resource-backing-on-X.patch deleted file mode 100644 index 28e3e9fc..00000000 --- a/patches/0001-drm-virtio-use-the-DMA-API-for-resource-backing-on-X.patch +++ /dev/null @@ -1,124 +0,0 @@ -From fe4973ef8a701733e42970a3be63c3335fd90cd0 Mon Sep 17 00:00:00 2001 -From: Ben Leggett -Date: Thu, 6 Aug 2026 15:51:03 -0400 -Subject: [PATCH] drm/virtio: use the DMA API for resource backing on Xen - -On a Xen PV domain page addressses bear no relation to the real machine -addresses the host would have to use to reach it. -virtio_ring.c handles this correctly, vring_use_dma_api() -returns true for any xen_domain() regardless of VIRTIO_F_ACCESS_PLATFORM. -virtio-gpu makes the same decision independently, but its copy -looks only at the feature bit: - - bool use_dma_api = !virtio_has_dma_quirk(vgdev->vdev); - -QEMU does not set iommu_platform on virtio-vga by default, so -VIRTIO_F_ACCESS_PLATFORM is not negotiated, use_dma_api is false, and -virtio_gpu_object_shmem_init() describes the framebuffer's backing pages -to the host with sg_phys(). Those are guest-physical addresses. In a PV -domain they resolve, on the host side, to pages belonging to some other -domain, so the host scans out unrelated memory. - -Fix is to move the decision into virtio_gpu_use_dma_api() and give it the -xen_domain() check, like vring_use_dma_api() has. It -additionally enables the dma_sync_sgtable_for_device() calls in -virtgpu_vq.c, which are required for correctness whenever swiotlb -is in play. - -Reproduced with a Xen 4.21 PV dom0 nested inside QEMU 8.2 with -virtio-vga, on both a distro 6.8 kernel and 6.18 LTS. A PVH dom0 -works fine and doesn't need this fix because it is identity-mapped, -only PV dom0s are affected. - -Signed-off-by: Ben Leggett ---- - drivers/gpu/drm/virtio/virtgpu_drv.h | 24 ++++++++++++++++++++++++ - drivers/gpu/drm/virtio/virtgpu_object.c | 2 +- - drivers/gpu/drm/virtio/virtgpu_vq.c | 6 +++--- - 3 files changed, 28 insertions(+), 4 deletions(-) - -diff --git a/drivers/gpu/drm/virtio/virtgpu_drv.h b/drivers/gpu/drm/virtio/virtgpu_drv.h -index 2f35319..d41367e 100644 ---- a/drivers/gpu/drm/virtio/virtgpu_drv.h -+++ b/drivers/gpu/drm/virtio/virtgpu_drv.h -@@ -43,6 +43,8 @@ - #include - #include - -+#include -+ - #define DRIVER_NAME "virtio_gpu" - #define DRIVER_DESC "virtio GPU" - -@@ -60,6 +62,24 @@ - /* See virtio_gpu_ctx_create. One additional character for NULL terminator. */ - #define DEBUG_NAME_MAX_LEN 65 - -+/* -+ * Whether the host must be told about resource backing pages by DMA address -+ * rather than guest-physical address. -+ * -+ * This mirrors vring_use_dma_api() in drivers/virtio/virtio_ring.c, including -+ * its xen_domain() case. -+ */ -+static inline bool virtio_gpu_use_dma_api(const struct virtio_device *vdev) -+{ -+ if (!virtio_has_dma_quirk(vdev)) -+ return true; -+ -+ if (xen_domain()) -+ return true; -+ -+ return false; -+} -+ - struct virtio_gpu_object_params { - unsigned long size; - bool dumb; -diff --git a/drivers/gpu/drm/virtio/virtgpu_object.c b/drivers/gpu/drm/virtio/virtgpu_object.c -index e6363c8..d228b5b 100644 ---- a/drivers/gpu/drm/virtio/virtgpu_object.c -+++ b/drivers/gpu/drm/virtio/virtgpu_object.c -@@ -161,7 +161,7 @@ static int virtio_gpu_object_shmem_init(struct virtio_gpu_device *vgdev, - struct virtio_gpu_mem_entry **ents, - unsigned int *nents) - { -- bool use_dma_api = !virtio_has_dma_quirk(vgdev->vdev); -+ bool use_dma_api = virtio_gpu_use_dma_api(vgdev->vdev); - struct scatterlist *sg; - struct sg_table *pages; - int si; -diff --git a/drivers/gpu/drm/virtio/virtgpu_vq.c b/drivers/gpu/drm/virtio/virtgpu_vq.c -index 83d46a8..4a72c69 100644 ---- a/drivers/gpu/drm/virtio/virtgpu_vq.c -+++ b/drivers/gpu/drm/virtio/virtgpu_vq.c -@@ -723,7 +723,7 @@ int virtio_gpu_panic_cmd_transfer_to_host_2d(struct virtio_gpu_device *vgdev, - struct virtio_gpu_object *bo = gem_to_virtio_gpu_obj(objs->objs[0]); - struct virtio_gpu_transfer_to_host_2d *cmd_p; - struct virtio_gpu_vbuffer *vbuf; -- bool use_dma_api = !virtio_has_dma_quirk(vgdev->vdev); -+ bool use_dma_api = virtio_gpu_use_dma_api(vgdev->vdev); - - if (virtio_gpu_is_shmem(bo) && use_dma_api) - dma_sync_sgtable_for_device(vgdev->vdev->dev.parent, -@@ -754,7 +754,7 @@ void virtio_gpu_cmd_transfer_to_host_2d(struct virtio_gpu_device *vgdev, - struct virtio_gpu_object *bo = gem_to_virtio_gpu_obj(objs->objs[0]); - struct virtio_gpu_transfer_to_host_2d *cmd_p; - struct virtio_gpu_vbuffer *vbuf; -- bool use_dma_api = !virtio_has_dma_quirk(vgdev->vdev); -+ bool use_dma_api = virtio_gpu_use_dma_api(vgdev->vdev); - - if (virtio_gpu_is_shmem(bo) && use_dma_api) - dma_sync_sgtable_for_device(vgdev->vdev->dev.parent, -@@ -1190,7 +1190,7 @@ void virtio_gpu_cmd_transfer_to_host_3d(struct virtio_gpu_device *vgdev, - struct virtio_gpu_object *bo = gem_to_virtio_gpu_obj(objs->objs[0]); - struct virtio_gpu_transfer_host_3d *cmd_p; - struct virtio_gpu_vbuffer *vbuf; -- bool use_dma_api = !virtio_has_dma_quirk(vgdev->vdev); -+ bool use_dma_api = virtio_gpu_use_dma_api(vgdev->vdev); - - if (virtio_gpu_is_shmem(bo) && use_dma_api) - dma_sync_sgtable_for_device(vgdev->vdev->dev.parent, --- -2.55.0 - diff --git a/patches/0001-feat-xen-deflate-balloon-via-oom-notifier.patch b/patches/0001-feat-xen-deflate-balloon-via-oom-notifier.patch deleted file mode 100644 index 88c9b7db..00000000 --- a/patches/0001-feat-xen-deflate-balloon-via-oom-notifier.patch +++ /dev/null @@ -1,97 +0,0 @@ -From 121220610624b3a773a7c78659637a877dc06353 Mon Sep 17 00:00:00 2001 -Message-ID: <121220610624b3a773a7c78659637a877dc06353.1786027798.git.alexander@edera.dev> -From: "Alexander M. Merritt" -Date: Wed, 5 Aug 2026 23:32:27 -0500 -Subject: [PATCH] feat(xen): deflate balloon via oom notifier - -Signed-off-by: Alexander M. Merritt ---- - drivers/xen/balloon.c | 58 +++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 58 insertions(+) - -diff --git a/drivers/xen/balloon.c b/drivers/xen/balloon.c -index 8c44a25a7d2b..85b6cfe53b28 100644 ---- a/drivers/xen/balloon.c -+++ b/drivers/xen/balloon.c -@@ -55,6 +55,7 @@ - #include - #include - #include -+#include - #include - #include - #include -@@ -679,6 +680,61 @@ void xen_free_ballooned_pages(unsigned int nr_pages, struct page **pages) - } - EXPORT_SYMBOL(xen_free_ballooned_pages); - -+#define BALLOON_OOM_DEFLATE_BATCH 512UL /* pages per OOM event (~2 MiB) */ -+ -+static int balloon_oom_notify(struct notifier_block *nb, unsigned long dummy, -+ void *parm) -+{ -+ unsigned long *freed = parm; -+ unsigned long before, nr; -+ -+ /* -+ * The balloon worker (or an inflate) may hold balloon_mutex and could -+ * itself be the allocation that triggered OOM; never block on it here. -+ * The mutex also guards the shared frame_list used by the populate path. -+ */ -+ if (!mutex_trylock(&balloon_mutex)) -+ return NOTIFY_OK; -+ -+ /* nr: number of pages parked on the balloon list that we can reclaim */ -+ nr = balloon_stats.balloon_low + balloon_stats.balloon_high; -+ -+ /* clamp, so we don't release all of them at once */ -+ nr = min_t(unsigned long, nr, BALLOON_OOM_DEFLATE_BATCH); -+ -+ /* if nr = 0, no pages remain to reclaim, OOM killer proceeds */ -+ -+ /* capture current before deflating, to calculate actual release */ -+ before = balloon_stats.current_pages; -+ -+ /* runs XENMEM_populate_physmap, updates current_pages */ -+ if (nr > 0) -+ increase_reservation(nr); -+ -+ /* -+ * increase_reservation() bumped current_pages by however many Xen -+ * actually populated (possibly fewer than asked, or zero if the host is -+ * out or we are already at max_pages). Report that to the OOM killer so -+ * it retries instead of killing when we made progress. -+ */ -+ *freed += balloon_stats.current_pages - before; -+ -+ /* -+ * Keep target_pages >= current_pages so -+ * balloon_process() will not immediately inflate the reclaimed pages back -+ * out and re-create the pressure. dom0 reconciles the higher figure. -+ */ -+ if (balloon_stats.target_pages < balloon_stats.current_pages) -+ balloon_stats.target_pages = balloon_stats.current_pages; -+ -+ mutex_unlock(&balloon_mutex); -+ return NOTIFY_OK; -+} -+ -+static struct notifier_block balloon_oom_nb = { -+ .notifier_call = balloon_oom_notify, -+}; -+ - static int __init balloon_add_regions(void) - { - unsigned long start_pfn, pages; -@@ -773,6 +829,8 @@ static int __init balloon_init(void) - /* Init the xen-balloon driver. */ - xen_balloon_init(); - -+ register_oom_notifier(&balloon_oom_nb); -+ - return 0; - - underflow: --- -2.48.1 - diff --git a/patches/0001-firmware-qemu_fw_cfg-do-not-use-the-DMA-interface-on.patch b/patches/0001-firmware-qemu_fw_cfg-do-not-use-the-DMA-interface-on.patch deleted file mode 100644 index 599dd3d4..00000000 --- a/patches/0001-firmware-qemu_fw_cfg-do-not-use-the-DMA-interface-on.patch +++ /dev/null @@ -1,67 +0,0 @@ -From 07e6e60199acdb20a7336020335e0b85fb41c683 Mon Sep 17 00:00:00 2001 -From: Ben Leggett -Date: Tue, 11 Aug 2026 13:55:35 -0400 -Subject: [PATCH] firmware: qemu_fw_cfg: do not use the DMA interface on Xen PV - -fw_cfg_dma_transfer() describes its descriptor and the caller's buffer to -the device with virt_to_phys(), on the stated assumption that the device -needs no IOMMU protection and therefore no address translation: - - *d = (struct fw_cfg_dma_access) { - .address = cpu_to_be64(address ? virt_to_phys(address) : 0), - ... - }; - dma = virt_to_phys(d); - -In a Xen PV domain that does not hold. A pseudo-physical address bears no -relation to the machine address the device model needs to reach the page. -The device never sees the descriptor and never clears its control word, so -fw_cfg_wait_for_control() spins on it with no timeout. Because the loop -never returns to userspace it also never dequeues a signal, so the task -cannot be killed and burns a CPU until the domain is reset. - -Blob reads go through the data register with ioread8_rep() and are -unaffected, so refuse only the DMA interface. fw_cfg_dma_enabled() has a -single caller, which already warns and continues when the vmcoreinfo write -fails. The cost on Xen PV is that the host cannot locate the guest's -vmcoreinfo note, which it could not do correctly there in any case. - -Fixes: 2d6d60a3d3ec ("fw_cfg: write vmcoreinfo details") -Signed-off-by: Ben Leggett ---- - drivers/firmware/qemu_fw_cfg.c | 12 ++++++++++++ - 1 file changed, 12 insertions(+) - -diff --git a/drivers/firmware/qemu_fw_cfg.c b/drivers/firmware/qemu_fw_cfg.c -index 0eebd572f9a5..e13f043f5f9a 100644 ---- a/drivers/firmware/qemu_fw_cfg.c -+++ b/drivers/firmware/qemu_fw_cfg.c -@@ -38,6 +38,7 @@ - #include - #include - #include -+#include - - MODULE_AUTHOR("Gabriel L. Somlo "); - MODULE_DESCRIPTION("QEMU fw_cfg sysfs support"); -@@ -70,6 +71,17 @@ static void fw_cfg_sel_endianness(u16 key) - #ifdef CONFIG_VMCORE_INFO - static inline bool fw_cfg_dma_enabled(void) - { -+ /* -+ * The DMA interface hands the device a virt_to_phys() address. -+ * In a Xen PV domain that is a pseudo-physical address, so the -+ * device writes the completion somewhere else entirely and -+ * fw_cfg_wait_for_control() spins forever. -+ * Reads go through the data register and are unaffected, so only -+ * the DMA interface is refused. -+ */ -+ if (xen_pv_domain()) -+ return false; -+ - return (fw_cfg_rev & FW_CFG_VERSION_DMA) && fw_cfg_reg_dma; - } - --- -2.55.0 - diff --git a/patches/0001-x86-CPU-AMD-avoid-printing-reset-reasons-on-Xen-domU.patch b/patches/0001-x86-CPU-AMD-avoid-printing-reset-reasons-on-Xen-domU.patch deleted file mode 100644 index 0824ca36..00000000 --- a/patches/0001-x86-CPU-AMD-avoid-printing-reset-reasons-on-Xen-domU.patch +++ /dev/null @@ -1,43 +0,0 @@ -From a0774aaf7f936980c06470233e1330b6d8fc9bca Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Thu, 4 Sep 2025 12:32:45 -0700 -Subject: [PATCH] x86/CPU/AMD: avoid printing reset reasons on Xen domU - -Xen domU cannot access the given MMIO address for security reasons, -resulting in a failed hypercall in ioremap() due to permissions. - -Fixes: ab8131028710 ("x86/CPU/AMD: Print the reason for the last reset") -Signed-off-by: Ariadne Conill -Cc: stable@vger.kernel.org -Signed-off-by: Ariadne Conill ---- - arch/x86/kernel/cpu/amd.c | 6 ++++++ - 1 file changed, 6 insertions(+) - -diff --git a/arch/x86/kernel/cpu/amd.c b/arch/x86/kernel/cpu/amd.c -index a6f88ca1a6b4..99308fba4d7d 100644 ---- a/arch/x86/kernel/cpu/amd.c -+++ b/arch/x86/kernel/cpu/amd.c -@@ -29,6 +29,8 @@ - # include - #endif - -+#include -+ - #include "cpu.h" - - u16 invlpgb_count_max __ro_after_init = 1; -@@ -1333,6 +1335,10 @@ static __init int print_s5_reset_status_mmio(void) - if (!cpu_feature_enabled(X86_FEATURE_ZEN)) - return 0; - -+ /* Xen PV domU cannot access hardware directly, so bail for domU case */ -+ if (cpu_feature_enabled(X86_FEATURE_XENPV) && !xen_initial_domain()) -+ return 0; -+ - addr = ioremap(FCH_PM_BASE + FCH_PM_S5_RESET_STATUS, sizeof(value)); - if (!addr) - return 0; --- -2.51.0 - diff --git a/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_01.patch b/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_01.patch deleted file mode 100644 index 727b68d7..00000000 --- a/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_01.patch +++ /dev/null @@ -1,33 +0,0 @@ -From 1826ef30bf0f780235245e1a02c5a50675344338 Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Tue, 3 Sep 2024 10:31:47 -0700 -Subject: [PATCH] x86/topology: Tolerate lack of APIC when booting as Xen domU - -Xen domU instances do not boot on x86 with ACPI enabled, so the entire -ACPI subsystem is ultimately disabled. This causes acpi_mps_check() -to trigger a warning that the ACPI MPS table is not present, which then -disables APIC support on domU, breaking the CPU topology detection for -all vCPUs other than the boot vCPU. - -Fixes: 7c0edad3643f ("x86/cpu/topology: Rework possible CPU management") -Signed-off-by: Ariadne Conill ---- - arch/x86/kernel/cpu/topology.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/arch/x86/kernel/cpu/topology.c b/arch/x86/kernel/cpu/topology.c -index 621a151ccf7d..38fa5ed816d6 100644 ---- a/arch/x86/kernel/cpu/topology.c -+++ b/arch/x86/kernel/cpu/topology.c -@@ -429,7 +429,7 @@ void __init topology_apply_cmdline_limits_early(void) - unsigned int possible = nr_cpu_ids; - - /* 'maxcpus=0' 'nosmp' 'nolapic' 'disableapic' 'noapic' */ -- if (!setup_max_cpus || ioapic_is_disabled || apic_is_disabled) -+ if (!setup_max_cpus || ioapic_is_disabled || (apic_is_disabled && !xen_pv_domain())) - possible = 1; - - /* 'possible_cpus=N' */ --- -2.39.2 - diff --git a/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_02.patch b/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_02.patch deleted file mode 100644 index 48521202..00000000 --- a/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_02.patch +++ /dev/null @@ -1,32 +0,0 @@ -From aed410e81c670d2d543732057e1e2fc030f2c586 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Sat, 21 Dec 2024 08:42:19 -0500 -Subject: [PATCH] x86/topology: Tolerate lack of APIC when booting as Xen domU - -Xen domU instances do not boot on x86 with ACPI enabled, so the entire -ACPI subsystem is ultimately disabled. This causes acpi_mps_check() -to trigger a warning that the ACPI MPS table is not present, which then -disables APIC support on domU, breaking the CPU topology detection for -all vCPUs other than the boot vCPU. - -Fixes: 7c0edad3643f ("x86/cpu/topology: Rework possible CPU management") ---- - arch/x86/kernel/cpu/topology.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/arch/x86/kernel/cpu/topology.c b/arch/x86/kernel/cpu/topology.c -index b2e313ea17bf..dbc638f6aee8 100644 ---- a/arch/x86/kernel/cpu/topology.c -+++ b/arch/x86/kernel/cpu/topology.c -@@ -429,7 +429,7 @@ void __init topology_apply_cmdline_limits_early(void) - unsigned int possible = nr_cpu_ids; - - /* 'maxcpus=0' 'nosmp' 'nolapic' 'disableapic' */ -- if (!setup_max_cpus || apic_is_disabled) -+ if (!setup_max_cpus || (apic_is_disabled && !xen_pv_domain())) - possible = 1; - - /* 'possible_cpus=N' */ --- -2.47.1 - diff --git a/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_03.patch b/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_03.patch deleted file mode 100644 index beff2e49..00000000 --- a/patches/0001-x86-topology-Tolerate-lack-of-APIC-when-booting-as-X_03.patch +++ /dev/null @@ -1,32 +0,0 @@ -From 4fa33c7cacb6c63006e1817319a99b140407cba2 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Thu, 10 Apr 2025 12:32:14 -0400 -Subject: [PATCH] x86/topology: Tolerate lack of APIC when booting as Xen domU - -Xen domU instances do not boot on x86 with ACPI enabled, so the entire -ACPI subsystem is ultimately disabled. This causes acpi_mps_check() -to trigger a warning that the ACPI MPS table is not present, which then -disables APIC support on domU, breaking the CPU topology detection for -all vCPUs other than the boot vCPU. - -Fixes: 7c0edad3643f ("x86/cpu/topology: Rework possible CPU management") ---- - arch/x86/kernel/cpu/topology.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/arch/x86/kernel/cpu/topology.c b/arch/x86/kernel/cpu/topology.c -index 01456236a6dd..8af9487755a1 100644 ---- a/arch/x86/kernel/cpu/topology.c -+++ b/arch/x86/kernel/cpu/topology.c -@@ -429,7 +429,7 @@ void __init topology_apply_cmdline_limits_early(void) - unsigned int possible = nr_cpu_ids; - - /* 'maxcpus=0' 'nosmp' 'nolapic' */ -- if (!setup_max_cpus || apic_is_disabled) -+ if (!setup_max_cpus || (apic_is_disabled && !xen_pv_domain())) - possible = 1; - - /* 'possible_cpus=N' */ --- -2.48.1 - diff --git a/patches/0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch b/patches/0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch deleted file mode 100644 index 813a7157..00000000 --- a/patches/0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch +++ /dev/null @@ -1,100 +0,0 @@ -From 35760e39333ee8b3786ca54eb8e23ad72358b035 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:49:47 -0700 -Subject: [PATCH 01/12] xen/evtchn: diagnose ring_overflow with post-barrier - cons re-read - -We've been seeing /dev/xen/evtchn fds enter the ring_overflow=1 -state under sustained event load (specifically: many ports bound -to a single fd, all firing concurrently during a daemon-side -metrics-collection storm). Once the flag latches, every read -returns -EFBIG and the fd is dead for event delivery until reset. - -The per-port masking invariant (each bound port can occupy at -most one ring slot, because lateeoi_ack_dynirq masks the channel -at Xen level before evtchn_interrupt runs and userspace can't -unmask until it reads + writes the port back) combined with the -ring being sized to nr_evtchns means overflow should not be -reachable. No "Interrupt for port N, but apparently not enabled" -WARNs precede the EFBIG state on hosts where this happens, so -the masking invariant is holding. - -One plausible candidate: the producer's READ_ONCE(ring_cons) in -evtchn_interrupt has no acquire pairing with the consumer's -WRITE_ONCE in evtchn_read -- they take ring_prod_lock and -ring_cons_mutex respectively, neither of which synchronizes -ring_cons between them. The smp_wmb/smp_rmb pair already -present covers ring entry visibility against ring_prod, not -ring_cons. A producer that fires right after the consumer -drains can observe a stale ring_cons and compute -(prod - stale_cons) >= ring_size when the ring actually has -room. On x86-TSO the staleness window is bounded to -store-buffer drain time but is non-zero; on weakly-ordered -hardware it's wider. - -Add an observe-only diagnostic at the overflow point: before -setting ring_overflow, issue an smp_mb() and re-read ring_cons. -Emit a rate-limited pr_warn with prod, both cons values, both -depths, ring_size, nr_evtchns, and the triggering port's -enabled/unbinding flags, tagging the log with " SPURIOUS" when -the post-barrier depth is below ring_size (i.e., the original -overflow detection was based on a stale view). - -This does not change runtime behavior other than emitting a log -line at the overflow boundary (which already required entering -a path that wedges the fd, so the printk cost is irrelevant). -If the diagnostic confirms the stale-cons hypothesis, the real -fix is to switch ring_cons access to smp_load_acquire / -smp_store_release pairing. - -Signed-off-by: Steven Noonan ---- - drivers/xen/evtchn.c | 31 ++++++++++++++++++++++++++++++- - 1 file changed, 30 insertions(+), 1 deletion(-) - -diff --git a/drivers/xen/evtchn.c b/drivers/xen/evtchn.c -index 7e4a13e632dc..85619b041ee8 100644 ---- a/drivers/xen/evtchn.c -+++ b/drivers/xen/evtchn.c -@@ -189,8 +189,37 @@ static irqreturn_t evtchn_interrupt(int irq, void *data) - kill_fasync(&u->evtchn_async_queue, - SIGIO, POLL_IN); - } -- } else -+ } else { -+ unsigned int cons_now; -+ -+ /* -+ * Diagnostic: the READ_ONCE of ring_cons above has no -+ * acquire pairing with the WRITE_ONCE in evtchn_read -- -+ * the producer holds ring_prod_lock and the consumer -+ * holds ring_cons_mutex, so they don't synchronize on -+ * ring_cons. On weakly-ordered hardware (and within -+ * the x86 store-buffer drain window) the producer can -+ * observe a stale ring_cons and conclude the ring is -+ * full when it actually has room. Re-read with a full -+ * barrier before declaring overflow and tag the log as -+ * SPURIOUS when the post-barrier read shows the ring -+ * had room -- that pins the cause on the cons -+ * visibility race rather than a real fill-to-capacity -+ * or a broken per-port masking invariant. -+ */ -+ smp_mb(); -+ cons_now = READ_ONCE(u->ring_cons); -+ -+ pr_warn_ratelimited( -+ "xen-evtchn overflow on %s: port=%u prod=%u cons=%u cons_after_mb=%u depth=%u depth_after_mb=%u ring_size=%u nr_evtchns=%u enabled=%d unbinding=%d%s\n", -+ u->name, evtchn->port, prod, cons, cons_now, -+ prod - cons, prod - cons_now, -+ u->ring_size, u->nr_evtchns, -+ (int)evtchn->enabled, (int)evtchn->unbinding, -+ (prod - cons_now) < u->ring_size ? " SPURIOUS" : ""); -+ - u->ring_overflow = 1; -+ } - - spin_unlock(&u->ring_prod_lock); - --- -2.55.0 - diff --git a/patches/0001-xen-xlate_mmu-relocate-remap_pfn-utils.patch b/patches/0001-xen-xlate_mmu-relocate-remap_pfn-utils.patch deleted file mode 100644 index afa4ed61..00000000 --- a/patches/0001-xen-xlate_mmu-relocate-remap_pfn-utils.patch +++ /dev/null @@ -1,70 +0,0 @@ -From 5bc298c60601161ec0f012d38e4cc021347b9831 Mon Sep 17 00:00:00 2001 -Message-ID: <5bc298c60601161ec0f012d38e4cc021347b9831.1779978695.git.alexander@edera.dev> -From: "Alexander M. Merritt" -Date: Wed, 27 May 2026 13:21:23 -0500 -Subject: [PATCH 1/2] xen: xlate_mmu: relocate remap_pfn utils - -Signed-off-by: Alexander M. Merritt ---- - drivers/xen/xlate_mmu.c | 38 +++++++++++++++++++------------------- - 1 file changed, 19 insertions(+), 19 deletions(-) - -diff --git a/drivers/xen/xlate_mmu.c b/drivers/xen/xlate_mmu.c -index f17c4c03db30..6a15396b08aa 100644 ---- a/drivers/xen/xlate_mmu.c -+++ b/drivers/xen/xlate_mmu.c -@@ -61,6 +61,25 @@ static void xen_for_each_gfn(struct page **pages, unsigned nr_gfn, - } - } - -+struct remap_pfn { -+ struct mm_struct *mm; -+ struct page **pages; -+ pgprot_t prot; -+ unsigned long i; -+}; -+ -+static int remap_pfn_fn(pte_t *ptep, unsigned long addr, void *data) -+{ -+ struct remap_pfn *r = data; -+ struct page *page = r->pages[r->i]; -+ pte_t pte = pte_mkspecial(pfn_pte(page_to_pfn(page), r->prot)); -+ -+ set_pte_at(r->mm, addr, ptep, pte); -+ r->i++; -+ -+ return 0; -+} -+ - struct remap_data { - xen_pfn_t *fgfn; /* foreign domain's gfn */ - int nr_fgfn; /* Number of foreign gfn left to map */ -@@ -262,25 +281,6 @@ int __init xen_xlate_map_ballooned_pages(xen_pfn_t **gfns, void **virt, - return 0; - } - --struct remap_pfn { -- struct mm_struct *mm; -- struct page **pages; -- pgprot_t prot; -- unsigned long i; --}; -- --static int remap_pfn_fn(pte_t *ptep, unsigned long addr, void *data) --{ -- struct remap_pfn *r = data; -- struct page *page = r->pages[r->i]; -- pte_t pte = pte_mkspecial(pfn_pte(page_to_pfn(page), r->prot)); -- -- set_pte_at(r->mm, addr, ptep, pte); -- r->i++; -- -- return 0; --} -- - /* Used by the privcmd module, but has to be built-in on ARM */ - int xen_remap_vma_range(struct vm_area_struct *vma, unsigned long addr, unsigned long len) - { --- -2.48.1 - diff --git a/patches/0002-x86-amd_node-fix-integer-divide-by-zero-during-init.patch b/patches/0002-x86-amd_node-fix-integer-divide-by-zero-during-init.patch deleted file mode 100644 index 70bd3642..00000000 --- a/patches/0002-x86-amd_node-fix-integer-divide-by-zero-during-init.patch +++ /dev/null @@ -1,51 +0,0 @@ -From 31d0c56ed10f08e0411a549c3398f7e3d93b899a Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Fri, 14 Nov 2025 10:34:21 -0800 -Subject: [PATCH 2/3] x86/amd_node: fix integer divide by zero during init - -On a Xen dom0 boot, this feature does not behave, and we end up -calculating: - - num_roots = 1 - num_nodes = 2 - roots_per_node = 0 - -This causes a divide-by-zero in the modulus inside the loop. - -This change adds a couple of guards for invalid states where we might -get a divide-by-zero. - -Signed-off-by: Steven Noonan -Signed-off-by: Ariadne Conill -CC: Yazen Ghannam -CC: x86@vger.kernel.org -CC: stable@vger.kernel.org ---- - arch/x86/kernel/amd_node.c | 11 +++++++++++ - 1 file changed, 11 insertions(+) - -diff --git a/arch/x86/kernel/amd_node.c b/arch/x86/kernel/amd_node.c -index 3d0a4768d603..cdc6ba224d4a 100644 ---- a/arch/x86/kernel/amd_node.c -+++ b/arch/x86/kernel/amd_node.c -@@ -282,6 +282,17 @@ static int __init amd_smn_init(void) - return -ENODEV; - - num_nodes = amd_num_nodes(); -+ -+ if (!num_nodes) -+ return -ENODEV; -+ -+ /* Possibly a virtualized environment (e.g. Xen) where we will get -+ * roots_per_node=0 if the number of roots is fewer than number of -+ * nodes -+ */ -+ if (num_roots < num_nodes) -+ return -ENODEV; -+ - amd_roots = kcalloc(num_nodes, sizeof(*amd_roots), GFP_KERNEL); - if (!amd_roots) - return -ENOMEM; --- -2.51.2 - diff --git a/patches/0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch b/patches/0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch deleted file mode 100644 index 870c21a0..00000000 --- a/patches/0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch +++ /dev/null @@ -1,268 +0,0 @@ -From b675cdf0b8448a460ff207a708aa272532b8a9a3 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:51:31 -0700 -Subject: [PATCH 02/12] xen: add xen_mfn_to_node to resolve a foreign frame's - host NUMA node - -Dom0 code that grant-maps foreign pages -- xenbus ring mappings, -gntdev -- has no way to learn which host NUMA node backs a foreign -frame, so nothing downstream (kthread and IRQ placement, placeholder -page homing) can be made node-local to the guest being served. Every -backend kthread for every queue piles onto whatever node it happens -to land on, regardless of the guest's vNUMA layout. - -Add xen_mfn_to_node(), resolving one host MFN to a Linux node id via -the new XENMEM_get_mfn_pxms hypercall, along with the hypercall's -interface definition. It lives in grant-table.c because its callers -are the grant-mapping paths that need to home placeholder pages. - -Three NUMA identifier namespaces are involved. Xen returns host PXM -(firmware-supplied), matching what dom0's own SRAT already uses; -pxm_to_node() converts to a Linux dom0 node id, which is what callers -feed to Linux helpers like cpumask_of_node() and -kthread_create_on_node(). Keeping the Xen-side ABI in PXM-space lets -callers translate with one standard lookup instead of maintaining -their own Xen-nid -> Linux-node table. - -The query is meaningful only in the hardware domain: Xen restricts -XENMEM_get_mfn_pxms to it, and a guest has no view of host topology -to localize against in any case. xen_mfn_to_node returns -NUMA_NO_NODE immediately when !xen_initial_domain(), without issuing -the hypercall. - -Older or non-Edera hypervisors lack the hypercall. The first call -returns -ENOSYS, which latches a global "unsupported" flag; every -subsequent call returns NUMA_NO_NODE without entering the -hypervisor. -EPERM (XSM policy, a late hardware domain) latches the -same way, so no boot pays more than one refused attempt. Callers -seeing NUMA_NO_NODE fall back to NUMA-oblivious behaviour, making the -kernel-side change safe to ship without a lockstep hypervisor update. - -Gated by CONFIG_XEN_BACKEND_NUMA_AFFINITY, which depends on -XEN_BACKEND, NUMA, ACPI_NUMA, and XEN_UNPOPULATED_ALLOC and defaults -to y. XEN_UNPOPULATED_ALLOC is a hard dependency rather than a soft -one because the placement work the rest of this series performs is -only meaningful when grant-map placeholders come from a per-node -pool: with the generic balloon allocator, page_to_nid() reflects only -where dom0's free RAM happened to be, and per-ring NUMA placement -would commit to wrong nodes confidently rather than silently no-op. -Disabling the option compiles the query out; the header supplies a -NUMA_NO_NODE stub so callers need no ifdefs. - -Sampling policy is left to callers. The hypercall accepts a batch of -MFNs, though the consumers in this series query only a ring's first -frame. - -Signed-off-by: Steven Noonan ---- - drivers/xen/Kconfig | 25 ++++++++++ - drivers/xen/grant-table.c | 86 ++++++++++++++++++++++++++++++++++ - include/xen/interface/memory.h | 26 ++++++++++ - include/xen/xen.h | 16 +++++++ - 4 files changed, 153 insertions(+) - -diff --git a/drivers/xen/Kconfig b/drivers/xen/Kconfig -index f9a35ed266ec..147e6acb231b 100644 ---- a/drivers/xen/Kconfig -+++ b/drivers/xen/Kconfig -@@ -96,6 +96,31 @@ config XEN_BACKEND - Support for backend device drivers that provide I/O services - to other virtual machines. - -+config XEN_BACKEND_NUMA_AFFINITY -+ bool "NUMA affinity for Xen backend drivers" -+ depends on XEN_BACKEND && NUMA && ACPI_NUMA && XEN_UNPOPULATED_ALLOC -+ default y -+ help -+ Allow Xen backend drivers (netback, blkback, gntdev consumers) -+ to discover the host NUMA node that hosts a grant-mapped ring -+ page, and to place their service threads and IRQs on that node. -+ -+ XEN_UNPOPULATED_ALLOC provides the per-node placeholder-page pool -+ the relocation logic in xenbus_map_ring_valloc() draws from. -+ Without it, placeholders come from the generic balloon allocator, -+ whose page_to_nid() reflects only where dom0's free RAM happened -+ to be -- not the host node of the foreign frame the placeholder -+ will end up backing. In that configuration the per-ring -+ placement decisions would be confidently wrong rather than just -+ absent, so the Kconfig hard-depends on XEN_UNPOPULATED_ALLOC -+ rather than silently degrading. -+ -+ Requires hypervisor support for the XENMEM_get_mfn_pxms -+ hypercall. Without that support the feature is silently a -+ no-op, equivalent to NUMA-oblivious behaviour. -+ -+ If unsure, say Y. -+ - config XENFS - tristate "Xen filesystem" - select XEN_PRIVCMD -diff --git a/drivers/xen/grant-table.c b/drivers/xen/grant-table.c -index 478d2ad725ac..f8b86a8679de 100644 ---- a/drivers/xen/grant-table.c -+++ b/drivers/xen/grant-table.c -@@ -67,6 +67,10 @@ - - #include - -+#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY -+#include -+#endif -+ - #define GNTTAB_LIST_END 0xffffffff - - static grant_ref_t **gnttab_list; -@@ -881,6 +885,88 @@ int gnttab_pages_set_private(int nr_pages, struct page **pages) - } - EXPORT_SYMBOL_GPL(gnttab_pages_set_private); - -+#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY -+/* -+ * Tri-state cache for XENMEM_get_mfn_pxms availability. -ENOSYS -+ * (hypervisor without the hypercall) or -EPERM (Xen restricts the -+ * query to the hardware domain) from the first attempt latches -+ * "unsupported", short-circuiting future calls. Any positive ACK -+ * (including the legitimate XEN_INVALID_NUMA_ID answer for an MFN Xen -+ * does not know about) latches "supported". -+ * -+ * Lock-free: at most one transition each direction, and "unsupported" -+ * is a stable terminal state once entered. A racing reader might -+ * issue one redundant hypercall before observing the cached state, -+ * which is harmless. -+ */ -+#define XEN_MFN_PXM_UNKNOWN 0 -+#define XEN_MFN_PXM_SUPPORTED 1 -+#define XEN_MFN_PXM_UNSUPPORTED 2 -+ -+static int xen_mfn_pxm_state = XEN_MFN_PXM_UNKNOWN; -+ -+/* -+ * Resolve one foreign MFN to a Linux node id. Returns NUMA_NO_NODE -+ * for any failure mode: hypercall unsupported, MFN unknown to Xen, -+ * PXM not registered with the dom0 ACPI namespace. -+ * -+ * Three NUMA identifier namespaces are involved here. Xen returns -+ * host PXM (firmware-supplied). pxm_to_node() translates to a Linux -+ * dom0 node id. Callers then use the result against Linux helpers -+ * like cpumask_of_node() and kthread_create_on_node(). -+ */ -+int xen_mfn_to_node(unsigned long mfn) -+{ -+ struct xen_get_mfn_pxms req; -+ xen_pfn_t mfn_arg = mfn; -+ uint32_t pxm = XEN_INVALID_NUMA_ID; -+ int rc; -+ -+ /* -+ * Xen answers this query only for the hardware domain. A domU -+ * mapping a foreign ring (a driver domain, say) would just earn -+ * an -EPERM per connect; skip the doomed hypercall and stay -+ * node-oblivious, which is the only honest answer for a guest -+ * with no view of host topology anyway. -+ */ -+ if (!xen_initial_domain()) -+ return NUMA_NO_NODE; -+ -+ if (READ_ONCE(xen_mfn_pxm_state) == XEN_MFN_PXM_UNSUPPORTED) -+ return NUMA_NO_NODE; -+ -+ memset(&req, 0, sizeof(req)); -+ set_xen_guest_handle(req.mfns, &mfn_arg); -+ set_xen_guest_handle(req.pxms, &pxm); -+ req.nr_mfns = 1; -+ -+ rc = HYPERVISOR_memory_op(XENMEM_get_mfn_pxms, &req); -+ if (rc < 0) { -+ /* -+ * Both errnos are stable properties of this boot: -ENOSYS -+ * means the hypervisor lacks the hypercall (no CONFIG_NUMA -+ * or too old), -EPERM that it refuses us despite the -+ * initial-domain check above (XSM policy, or a late -+ * hardware domain). Latch either so we never retry. -+ */ -+ if (rc == -ENOSYS || rc == -EPERM) { -+ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_UNSUPPORTED); -+ pr_info("XENMEM_get_mfn_pxms unavailable (%d), NUMA affinity for grant mappings disabled\n", -+ rc); -+ } -+ return NUMA_NO_NODE; -+ } -+ -+ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_SUPPORTED); -+ -+ if (pxm == XEN_INVALID_NUMA_ID) -+ return NUMA_NO_NODE; -+ -+ return pxm_to_node(pxm); -+} -+EXPORT_SYMBOL_GPL(xen_mfn_to_node); -+#endif /* CONFIG_XEN_BACKEND_NUMA_AFFINITY */ -+ - /** - * gnttab_alloc_pages - alloc pages suitable for grant mapping into - * @nr_pages: number of pages to alloc -diff --git a/include/xen/interface/memory.h b/include/xen/interface/memory.h -index 1a371a825c55..1998a12a9465 100644 ---- a/include/xen/interface/memory.h -+++ b/include/xen/interface/memory.h -@@ -325,4 +325,30 @@ struct xen_mem_acquire_resource { - }; - DEFINE_GUEST_HANDLE_STRUCT(xen_mem_acquire_resource); - -+/* -+ * XENMEM_get_mfn_pxms: resolve a batch of host MFNs to their firmware -+ * proximity-domain identifiers (host PXM on x86 ACPI). -+ * -+ * Returned values are in the host PXM namespace (the same value space -+ * dom0's own SRAT uses), not Xen's internal node id. Callers convert -+ * to a Linux node id with pxm_to_node(). Slots Xen has no node info -+ * for receive XEN_INVALID_NUMA_ID rather than failing the whole batch. -+ * -+ * Restricted to the hardware domain. On hypervisors that do not -+ * provide this op (older or non-Edera Xen, or builds without -+ * CONFIG_NUMA), the hypercall returns -ENOSYS; callers treat that as -+ * "feature unavailable" and fall back to NUMA-oblivious behaviour. -+ */ -+#define XENMEM_get_mfn_pxms 40 -+ -+#define XEN_INVALID_NUMA_ID (~(uint32_t)0) -+ -+struct xen_get_mfn_pxms { -+ GUEST_HANDLE(xen_pfn_t) mfns; -+ GUEST_HANDLE(uint32_t) pxms; -+ uint32_t nr_mfns; -+ uint32_t flags; -+}; -+DEFINE_GUEST_HANDLE_STRUCT(xen_get_mfn_pxms); -+ - #endif /* __XEN_PUBLIC_MEMORY_H__ */ -diff --git a/include/xen/xen.h b/include/xen/xen.h -index f280c5dcf923..f7f3dd7a3abe 100644 ---- a/include/xen/xen.h -+++ b/include/xen/xen.h -@@ -89,6 +89,22 @@ static inline void xen_free_unpopulated_pages(unsigned int nr_pages, - } - #endif - -+/* -+ * Resolve a foreign frame's host MFN to the Linux node id of the memory -+ * backing it, via XENMEM_get_mfn_pxms. NUMA_NO_NODE for any failure -+ * mode (hypercall unsupported or refused, MFN unknown to Xen, PXM not -+ * in the ACPI namespace) -- callers degrade to node-oblivious behaviour. -+ */ -+#include -+#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY -+int xen_mfn_to_node(unsigned long mfn); -+#else -+static inline int xen_mfn_to_node(unsigned long mfn) -+{ -+ return NUMA_NO_NODE; -+} -+#endif -+ - #if defined(CONFIG_XEN_DOM0) && defined(CONFIG_ACPI) && defined(CONFIG_X86) - bool __init xen_processor_present(uint32_t acpi_id); - #else --- -2.55.0 - diff --git a/patches/0002-xen-xlate_mmu-batch-gfn-mappings.patch b/patches/0002-xen-xlate_mmu-batch-gfn-mappings.patch deleted file mode 100644 index 991a27f1..00000000 --- a/patches/0002-xen-xlate_mmu-batch-gfn-mappings.patch +++ /dev/null @@ -1,197 +0,0 @@ -From 0b3410862d1c9a6be6973314f6979cf309b13e13 Mon Sep 17 00:00:00 2001 -Message-ID: <0b3410862d1c9a6be6973314f6979cf309b13e13.1779978695.git.alexander@edera.dev> -In-Reply-To: <5bc298c60601161ec0f012d38e4cc021347b9831.1779978695.git.alexander@edera.dev> -References: <5bc298c60601161ec0f012d38e4cc021347b9831.1779978695.git.alexander@edera.dev> -From: "Alexander M. Merritt" -Date: Wed, 27 May 2026 13:47:13 -0500 -Subject: [PATCH 2/2] xen: xlate_mmu: batch gfn mappings - -PVH dom0 wanting to map in guest memory would iterate over each page -individually invoking hypercall add_to_physmap_range, leading to high -mapping latencies. - -Refactor xen_xlate_remap_gfn_array to separately batch GFN mappings to -the hypervisor to amortize hypercall overhead. - -Signed-off-by: Alexander M. Merritt ---- - drivers/xen/xlate_mmu.c | 133 +++++++++++++++++++++++----------------- - 1 file changed, 76 insertions(+), 57 deletions(-) - -diff --git a/drivers/xen/xlate_mmu.c b/drivers/xen/xlate_mmu.c -index 6a15396b08aa..f8dd332b7f46 100644 ---- a/drivers/xen/xlate_mmu.c -+++ b/drivers/xen/xlate_mmu.c -@@ -42,6 +42,9 @@ - #include - #include - -+/* Issue up to this many GFN mapping requests in a batch at a time. */ -+#define XLATE_BATCH_SIZE 16 -+ - typedef void (*xen_gfn_fn_t)(unsigned long gfn, void *data); - - /* Break down the pages in 4KB chunk and call fn for each gfn */ -@@ -92,12 +95,19 @@ struct remap_data { - int *err_ptr; - int mapped; - -- /* Hypercall parameters */ -- int h_errs[XEN_PFN_PER_PAGE]; -- xen_ulong_t h_idxs[XEN_PFN_PER_PAGE]; -- xen_pfn_t h_gpfns[XEN_PFN_PER_PAGE]; -+ /* Hypercall parameters. -+ * -+ * idxs: source GFNs in foreign domain P2M (combined with XENMAPSPACE_gmfn_foreign) -+ * gpfns: destination GFNs in dom0's P2M. Extracted from pages[] -+ * -+ * h_idxs and h_gpfns exist as pairs: we map into dom0's GFN -+ * the same MFN for the GFN in the foreign domain. -+ */ -+ int h_errs[XLATE_BATCH_SIZE]; -+ xen_ulong_t h_idxs[XLATE_BATCH_SIZE]; -+ xen_pfn_t h_gpfns[XLATE_BATCH_SIZE]; - -- int h_iter; /* Iterator */ -+ int h_iter; - }; - - static void setup_hparams(unsigned long gfn, void *data) -@@ -112,53 +122,6 @@ static void setup_hparams(unsigned long gfn, void *data) - info->fgfn++; - } - --static int remap_pte_fn(pte_t *ptep, unsigned long addr, void *data) --{ -- struct remap_data *info = data; -- struct page *page = info->pages[info->index++]; -- pte_t pte = pte_mkspecial(pfn_pte(page_to_pfn(page), info->prot)); -- int rc, nr_gfn; -- uint32_t i; -- struct xen_add_to_physmap_range xatp = { -- .domid = DOMID_SELF, -- .foreign_domid = info->domid, -- .space = XENMAPSPACE_gmfn_foreign, -- }; -- -- nr_gfn = min_t(typeof(info->nr_fgfn), XEN_PFN_PER_PAGE, info->nr_fgfn); -- info->nr_fgfn -= nr_gfn; -- -- info->h_iter = 0; -- xen_for_each_gfn(&page, nr_gfn, setup_hparams, info); -- BUG_ON(info->h_iter != nr_gfn); -- -- set_xen_guest_handle(xatp.idxs, info->h_idxs); -- set_xen_guest_handle(xatp.gpfns, info->h_gpfns); -- set_xen_guest_handle(xatp.errs, info->h_errs); -- xatp.size = nr_gfn; -- -- rc = HYPERVISOR_memory_op(XENMEM_add_to_physmap_range, &xatp); -- -- /* info->err_ptr expect to have one error status per Xen PFN */ -- for (i = 0; i < nr_gfn; i++) { -- int err = (rc < 0) ? rc : info->h_errs[i]; -- -- *(info->err_ptr++) = err; -- if (!err) -- info->mapped++; -- } -- -- /* -- * Note: The hypercall will return 0 in most of the case if even if -- * all the fgmfn are not mapped. We still have to update the pte -- * as the userspace may decide to continue. -- */ -- if (!rc) -- set_pte_at(info->vma->vm_mm, addr, ptep, pte); -- -- return 0; --} -- - int xen_xlate_remap_gfn_array(struct vm_area_struct *vma, - unsigned long addr, - xen_pfn_t *gfn, int nr, -@@ -166,9 +129,10 @@ int xen_xlate_remap_gfn_array(struct vm_area_struct *vma, - unsigned domid, - struct page **pages) - { -- int err; - struct remap_data data; -- unsigned long range = DIV_ROUND_UP(nr, XEN_PFN_PER_PAGE) << PAGE_SHIFT; -+ int page_off = 0; -+ -+ BUILD_BUG_ON(XLATE_BATCH_SIZE % XEN_PFN_PER_PAGE != 0); - - /* Kept here for the purpose of making sure code doesn't break - x86 PVOPS */ -@@ -184,9 +148,64 @@ int xen_xlate_remap_gfn_array(struct vm_area_struct *vma, - data.err_ptr = err_ptr; - data.mapped = 0; - -- err = apply_to_page_range(vma->vm_mm, addr, range, -- remap_pte_fn, &data); -- return err < 0 ? err : data.mapped; -+ while (data.nr_fgfn > 0) { -+ int batch = min_t(int, XLATE_BATCH_SIZE, data.nr_fgfn); /* xen GFN units */ -+ /* NOTE: on ARM, page size may be larger than in Xen */ -+ int batch_pages = DIV_ROUND_UP(batch, XEN_PFN_PER_PAGE); -+ unsigned long batch_range = (unsigned long)batch_pages << PAGE_SHIFT; -+ -+ struct xen_add_to_physmap_range xatp = { -+ .domid = DOMID_SELF, -+ .foreign_domid = domid, -+ .space = XENMAPSPACE_gmfn_foreign, -+ .size = batch, -+ }; -+ -+ int rc, i; -+ -+ data.h_iter = 0; -+ xen_for_each_gfn(&pages[page_off], batch, setup_hparams, &data); -+ data.nr_fgfn -= batch; -+ -+ set_xen_guest_handle(xatp.idxs, data.h_idxs); -+ set_xen_guest_handle(xatp.gpfns, data.h_gpfns); -+ set_xen_guest_handle(xatp.errs, data.h_errs); -+ -+ rc = HYPERVISOR_memory_op(XENMEM_add_to_physmap_range, &xatp); -+ -+ /* -+ * Note: The hypercall will return 0 in most of the case if even if -+ * all the fgmfn are not mapped. We still have to update the pte -+ * as the userspace may decide to continue. -+ */ -+ if (rc == 0) { -+ struct remap_pfn r = { -+ .mm = vma->vm_mm, -+ .pages = &pages[page_off], -+ .prot = prot, -+ .i = 0, -+ }; -+ int walk_err = apply_to_page_range(vma->vm_mm, addr, -+ batch_range, -+ remap_pfn_fn, &r); -+ if (walk_err < 0) -+ return walk_err; -+ } -+ -+ /* err_ptr expected to have one error status per Xen PFN */ -+ for (i = 0; i < batch; i++) { -+ int err = (rc < 0) ? rc : data.h_errs[i]; -+ *(data.err_ptr++) = err; -+ if (!err) -+ data.mapped++; -+ } -+ -+ addr += batch_range; -+ page_off += batch_pages; -+ cond_resched(); -+ } -+ -+ return data.mapped; - } - EXPORT_SYMBOL_GPL(xen_xlate_remap_gfn_array); - --- -2.48.1 - diff --git a/patches/0003-x86-amd_node-fix-null-pointer-dereference-if-amd_smn.patch b/patches/0003-x86-amd_node-fix-null-pointer-dereference-if-amd_smn.patch deleted file mode 100644 index 75b3b17a..00000000 --- a/patches/0003-x86-amd_node-fix-null-pointer-dereference-if-amd_smn.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 33ecda262c724d29f589bafb335c1afc4f47bdd7 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Fri, 14 Nov 2025 11:41:48 -0800 -Subject: [PATCH 3/3] x86/amd_node: fix null pointer dereference if - amd_smn_init failed - -We should be checking the `smn_exclusive` flag before anything else, -because that indicates whether we got through `amd_smn_init` -successfully. - -Without this change, we dereference `amd_roots` even though it may not -be allocated. - -Signed-off-by: Steven Noonan -Signed-off-by: Ariadne Conill -CC: Yazen Ghannam -CC: x86@vger.kernel.org -CC: stable@vger.kernel.org ---- - arch/x86/kernel/amd_node.c | 6 +++--- - 1 file changed, 3 insertions(+), 3 deletions(-) - -diff --git a/arch/x86/kernel/amd_node.c b/arch/x86/kernel/amd_node.c -index cdc6ba224d4a..919932339f4a 100644 ---- a/arch/x86/kernel/amd_node.c -+++ b/arch/x86/kernel/amd_node.c -@@ -88,6 +88,9 @@ static int __amd_smn_rw(u8 i_off, u8 d_off, u16 node, u32 address, u32 *value, b - struct pci_dev *root; - int err = -ENODEV; - -+ if (!smn_exclusive) -+ return err; -+ - if (node >= amd_num_nodes()) - return err; - -@@ -95,9 +98,6 @@ static int __amd_smn_rw(u8 i_off, u8 d_off, u16 node, u32 address, u32 *value, b - if (!root) - return err; - -- if (!smn_exclusive) -- return err; -- - guard(mutex)(&smn_mutex); - - err = pci_write_config_dword(root, i_off, address); --- -2.51.2 - diff --git a/patches/0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch b/patches/0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch deleted file mode 100644 index aabd7465..00000000 --- a/patches/0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch +++ /dev/null @@ -1,279 +0,0 @@ -From 56ea4dae7db066a61b6c15a0afd3bdad55e38ad3 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:51:49 -0700 -Subject: [PATCH 03/12] xen: make xen_alloc_unpopulated_pages NUMA-aware - -xen_alloc_unpopulated_pages today hands out ZONE_DEVICE placeholder -pages from a single global free list, with the underlying section -registered via memremap_pages(pgmap, NUMA_NO_NODE). page_to_nid() -of any such page reports NUMA_NO_NODE, so every consumer of -foreign-mapped grant pages -- xenbus rings, gntdev mmaps, xlate_mmu, -privcmd, xen-drm -- has no useful node information for kernel code -that consults page_to_nid (slab placement decisions, autonuma, -numastat, various softirq routing heuristics). - -Partition the free list by Linux node id and register each -section-aligned IOMEM allocation against a specific node. Pages -drawn from page_list[N] then report page_to_nid() == N by -construction. Callers that know the host node of the foreign frame -they intend to grant-map can request from the matching pool; the -foreign frame's node and the placeholder page's nid then agree. - -Two exported entry points: - - xen_alloc_unpopulated_pages_node(nr, pages, node) -- new, takes an - explicit node preference. NUMA_NO_NODE and out-of-range values - are clamped to numa_node_id() at the boundary; internal code only - ever indexes into a valid slot. - - xen_alloc_unpopulated_pages(nr, pages) -- existing API, now a - wrapper that passes numa_node_id(). Strictly an improvement over - NUMA_NO_NODE for callers with no node hint of their own: the - caller's local node is at least as good a guess as none. - -xen_free_unpopulated_pages routes each page back to the pool of the -node it came from via page_to_nid(). No new arguments; existing -callers do not change. - -A single mutex still covers all per-node lists. Allocation and free -are connect-time events on I/O backends and on map_grant_ref ioctls, -not contention-sensitive paths. - -MAX_NUMNODES sizes the two arrays; on CONFIG_NUMA=n that is 1 and the -behaviour is byte-for-byte equivalent to the previous single-list -implementation. The consumers that pair this allocator with -xen_mfn_to_node to home placeholders on the foreign frame's node come -in the patches that follow. - -Signed-off-by: Steven Noonan ---- - drivers/xen/unpopulated-alloc.c | 103 +++++++++++++++++++++++++------- - include/xen/xen.h | 7 +++ - 2 files changed, 89 insertions(+), 21 deletions(-) - -diff --git a/drivers/xen/unpopulated-alloc.c b/drivers/xen/unpopulated-alloc.c -index 1dc0b495c8e5..ea9c76895459 100644 ---- a/drivers/xen/unpopulated-alloc.c -+++ b/drivers/xen/unpopulated-alloc.c -@@ -4,6 +4,7 @@ - #include - #include - #include -+#include - #include - - #include -@@ -12,12 +13,31 @@ - #include - #include - -+/* -+ * Free pages are kept on per-node lists indexed by Linux node id. Each -+ * fill_list() call grabs a fresh section-aligned IOMEM region and -+ * registers it with memremap_pages() against a specific node, so all -+ * struct pages in that section report that node via page_to_nid(). -+ * Backends that learn the host node of a foreign frame (via -+ * xenbus_ring_host_node) can request placeholder pages from the -+ * matching pool so page_to_nid agrees with the actual host placement. -+ * -+ * A single mutex covers all per-node lists. Alloc/free are -+ * connect-time events on the I/O backends and not contention-sensitive. -+ */ - static DEFINE_MUTEX(list_lock); --static struct page *page_list; --static unsigned int list_count; -+static struct page *page_list[MAX_NUMNODES]; -+static unsigned int list_count[MAX_NUMNODES]; - - static struct resource *target_resource; - -+static int xen_unpopulated_clamp_node(int node) -+{ -+ if (node == NUMA_NO_NODE || node < 0 || node >= MAX_NUMNODES) -+ return numa_node_id(); -+ return node; -+} -+ - /* Pages to subtract from the memory count when setting balloon target. */ - unsigned long xen_unpopulated_pages __initdata; - -@@ -34,7 +54,7 @@ int __weak __init arch_xen_unpopulated_init(struct resource **res) - return 0; - } - --static int fill_list(unsigned int nr_pages) -+static int fill_list(unsigned int nr_pages, int node) - { - struct dev_pagemap *pgmap; - struct resource *res, *tmp_res = NULL; -@@ -121,7 +141,15 @@ static int fill_list(unsigned int nr_pages) - } - #endif - -- vaddr = memremap_pages(pgmap, NUMA_NO_NODE); -+ /* -+ * Register the section against @node so page_to_nid() of any -+ * page in this section reports that value. Grant-map operations -+ * later install foreign MFNs into these slots; as long as the -+ * caller picks the section matching the foreign MFN's host node -+ * (which is the contract callers of xen_alloc_unpopulated_pages_node -+ * are expected to honour), page_to_nid is correct by construction. -+ */ -+ vaddr = memremap_pages(pgmap, node); - if (IS_ERR(vaddr)) { - pr_err("Cannot remap memory range\n"); - ret = PTR_ERR(vaddr); -@@ -131,9 +159,9 @@ static int fill_list(unsigned int nr_pages) - for (i = 0; i < alloc_pages; i++) { - struct page *pg = virt_to_page(vaddr + PAGE_SIZE * i); - -- pg->zone_device_data = page_list; -- page_list = pg; -- list_count++; -+ pg->zone_device_data = page_list[node]; -+ page_list[node] = pg; -+ list_count[node]++; - } - - return 0; -@@ -153,12 +181,21 @@ static int fill_list(unsigned int nr_pages) - } - - /** -- * xen_alloc_unpopulated_pages - alloc unpopulated pages -+ * xen_alloc_unpopulated_pages_node - alloc unpopulated pages on a node - * @nr_pages: Number of pages - * @pages: pages returned -- * @return 0 on success, error otherwise -+ * @node: Preferred Linux node id, or NUMA_NO_NODE for current CPU's node -+ * -+ * The returned pages are drawn from a per-node pool registered with -+ * memremap_pages() against @node, so page_to_nid() reports @node for -+ * every returned page. Callers that know the host node of a foreign -+ * frame should pass it here to keep page_to_nid in agreement with the -+ * actual host placement after a subsequent grant-map. -+ * -+ * Returns 0 on success, error otherwise. - */ --int xen_alloc_unpopulated_pages(unsigned int nr_pages, struct page **pages) -+int xen_alloc_unpopulated_pages_node(unsigned int nr_pages, struct page **pages, -+ int node) - { - unsigned int i; - int ret = 0; -@@ -171,19 +208,21 @@ int xen_alloc_unpopulated_pages(unsigned int nr_pages, struct page **pages) - if (!target_resource) - return xen_alloc_ballooned_pages(nr_pages, pages); - -+ node = xen_unpopulated_clamp_node(node); -+ - mutex_lock(&list_lock); -- if (list_count < nr_pages) { -- ret = fill_list(nr_pages - list_count); -+ if (list_count[node] < nr_pages) { -+ ret = fill_list(nr_pages - list_count[node], node); - if (ret) - goto out; - } - - for (i = 0; i < nr_pages; i++) { -- struct page *pg = page_list; -+ struct page *pg = page_list[node]; - - BUG_ON(!pg); -- page_list = pg->zone_device_data; -- list_count--; -+ page_list[node] = pg->zone_device_data; -+ list_count[node]--; - pages[i] = pg; - - #ifdef CONFIG_XEN_HAVE_PVMMU -@@ -193,9 +232,9 @@ int xen_alloc_unpopulated_pages(unsigned int nr_pages, struct page **pages) - unsigned int j; - - for (j = 0; j <= i; j++) { -- pages[j]->zone_device_data = page_list; -- page_list = pages[j]; -- list_count++; -+ pages[j]->zone_device_data = page_list[node]; -+ page_list[node] = pages[j]; -+ list_count[node]++; - } - goto out; - } -@@ -207,12 +246,32 @@ int xen_alloc_unpopulated_pages(unsigned int nr_pages, struct page **pages) - mutex_unlock(&list_lock); - return ret; - } -+EXPORT_SYMBOL(xen_alloc_unpopulated_pages_node); -+ -+/** -+ * xen_alloc_unpopulated_pages - alloc unpopulated pages -+ * @nr_pages: Number of pages -+ * @pages: pages returned -+ * @return 0 on success, error otherwise -+ * -+ * Equivalent to xen_alloc_unpopulated_pages_node() with the current -+ * CPU's node as the preference. -+ */ -+int xen_alloc_unpopulated_pages(unsigned int nr_pages, struct page **pages) -+{ -+ return xen_alloc_unpopulated_pages_node(nr_pages, pages, numa_node_id()); -+} - EXPORT_SYMBOL(xen_alloc_unpopulated_pages); - - /** - * xen_free_unpopulated_pages - return unpopulated pages - * @nr_pages: Number of pages - * @pages: pages to return -+ * -+ * Each page returns to the pool of the node it was originally allocated -+ * from, identified via page_to_nid(). Sections registered to a -+ * specific node yield pages whose nid reflects that node, so freed -+ * pages naturally land back in the matching list. - */ - void xen_free_unpopulated_pages(unsigned int nr_pages, struct page **pages) - { -@@ -225,9 +284,11 @@ void xen_free_unpopulated_pages(unsigned int nr_pages, struct page **pages) - - mutex_lock(&list_lock); - for (i = 0; i < nr_pages; i++) { -- pages[i]->zone_device_data = page_list; -- page_list = pages[i]; -- list_count++; -+ int node = xen_unpopulated_clamp_node(page_to_nid(pages[i])); -+ -+ pages[i]->zone_device_data = page_list[node]; -+ page_list[node] = pages[i]; -+ list_count[node]++; - } - mutex_unlock(&list_lock); - } -diff --git a/include/xen/xen.h b/include/xen/xen.h -index f7f3dd7a3abe..ccf9bfa33813 100644 ---- a/include/xen/xen.h -+++ b/include/xen/xen.h -@@ -70,6 +70,8 @@ extern u64 xen_saved_max_mem_size; - - #ifdef CONFIG_XEN_UNPOPULATED_ALLOC - extern unsigned long xen_unpopulated_pages; -+int xen_alloc_unpopulated_pages_node(unsigned int nr_pages, struct page **pages, -+ int node); - int xen_alloc_unpopulated_pages(unsigned int nr_pages, struct page **pages); - void xen_free_unpopulated_pages(unsigned int nr_pages, struct page **pages); - #include -@@ -77,6 +79,11 @@ int arch_xen_unpopulated_init(struct resource **res); - #else - #define xen_unpopulated_pages 0UL - #include -+static inline int xen_alloc_unpopulated_pages_node(unsigned int nr_pages, -+ struct page **pages, int node) -+{ -+ return xen_alloc_ballooned_pages(nr_pages, pages); -+} - static inline int xen_alloc_unpopulated_pages(unsigned int nr_pages, - struct page **pages) - { --- -2.55.0 - diff --git a/patches/0004-xen-grant-table-add-gnttab_alloc_pages_node.patch b/patches/0004-xen-grant-table-add-gnttab_alloc_pages_node.patch deleted file mode 100644 index c6ab059c..00000000 --- a/patches/0004-xen-grant-table-add-gnttab_alloc_pages_node.patch +++ /dev/null @@ -1,95 +0,0 @@ -From 0051d0715d03b8515e26ab5c01144a7d61017216 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:51:51 -0700 -Subject: [PATCH 04/12] xen/grant-table: add gnttab_alloc_pages_node - -gnttab_alloc_pages draws grant-mapping placeholder pages via -xen_alloc_unpopulated_pages, which prefers the calling CPU's NUMA -node. That is the right default for a caller with no better -information, but a consumer that already knows the host node of the -foreign frames it is about to map -- because it queried the node via -xen_mfn_to_node after a first map -- has no way to ask for -placeholders from the matching pool. - -Add gnttab_alloc_pages_node, taking an explicit node preference and -passing it through to xen_alloc_unpopulated_pages_node, and reduce -gnttab_alloc_pages to a wrapper passing NUMA_NO_NODE (which the -unpopulated allocator clamps to the local node, preserving today's -behaviour exactly). Frees are unchanged: gnttab_free_pages already -routes each page back to its home pool via page_to_nid. - -No functional change for existing callers. The first user is gntdev, -which will relocate a fresh grant mapping's placeholders onto the -foreign frames' node the same way the xenbus ring path gains later in -this series. - -Signed-off-by: Steven Noonan ---- - drivers/xen/grant-table.c | 25 ++++++++++++++++++++++--- - include/xen/grant_table.h | 1 + - 2 files changed, 23 insertions(+), 3 deletions(-) - -diff --git a/drivers/xen/grant-table.c b/drivers/xen/grant-table.c -index f8b86a8679de..879e08104805 100644 ---- a/drivers/xen/grant-table.c -+++ b/drivers/xen/grant-table.c -@@ -968,15 +968,23 @@ EXPORT_SYMBOL_GPL(xen_mfn_to_node); - #endif /* CONFIG_XEN_BACKEND_NUMA_AFFINITY */ - - /** -- * gnttab_alloc_pages - alloc pages suitable for grant mapping into -+ * gnttab_alloc_pages_node - alloc pages suitable for grant mapping into, -+ * drawn from a specific NUMA node's placeholder pool - * @nr_pages: number of pages to alloc - * @pages: returns the pages -+ * @node: node to draw the pages from, or NUMA_NO_NODE for the caller's -+ * local node -+ * -+ * A caller that knows the host node of the foreign frames it is about -+ * to map can request placeholders whose page_to_nid() matches; the -+ * generic gnttab_alloc_pages() below has no such knowledge and settles -+ * for the local node. - */ --int gnttab_alloc_pages(int nr_pages, struct page **pages) -+int gnttab_alloc_pages_node(int nr_pages, struct page **pages, int node) - { - int ret; - -- ret = xen_alloc_unpopulated_pages(nr_pages, pages); -+ ret = xen_alloc_unpopulated_pages_node(nr_pages, pages, node); - if (ret < 0) - return ret; - -@@ -986,6 +994,17 @@ int gnttab_alloc_pages(int nr_pages, struct page **pages) - - return ret; - } -+EXPORT_SYMBOL_GPL(gnttab_alloc_pages_node); -+ -+/** -+ * gnttab_alloc_pages - alloc pages suitable for grant mapping into -+ * @nr_pages: number of pages to alloc -+ * @pages: returns the pages -+ */ -+int gnttab_alloc_pages(int nr_pages, struct page **pages) -+{ -+ return gnttab_alloc_pages_node(nr_pages, pages, NUMA_NO_NODE); -+} - EXPORT_SYMBOL_GPL(gnttab_alloc_pages); - - #ifdef CONFIG_XEN_UNPOPULATED_ALLOC -diff --git a/include/xen/grant_table.h b/include/xen/grant_table.h -index 69ac6d80a006..d13566a00ce7 100644 ---- a/include/xen/grant_table.h -+++ b/include/xen/grant_table.h -@@ -211,6 +211,7 @@ void gnttab_free_auto_xlat_frames(void); - - #define gnttab_map_vaddr(map) ((void *)(map.host_virt_addr)) - -+int gnttab_alloc_pages_node(int nr_pages, struct page **pages, int node); - int gnttab_alloc_pages(int nr_pages, struct page **pages); - void gnttab_free_pages(int nr_pages, struct page **pages); - --- -2.55.0 - diff --git a/patches/0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch b/patches/0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch deleted file mode 100644 index c3bf8696..00000000 --- a/patches/0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch +++ /dev/null @@ -1,298 +0,0 @@ -From 9aaace179740c21966407a4e241d83c03214ea21 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:51:53 -0700 -Subject: [PATCH 05/12] xen/events: add _on_node variants of the lateeoi bind - helpers - -xen_allocate_irq_dynamic() unconditionally calls -irq_alloc_desc_from(0, -1), so every Xen evtchn IRQ descriptor is -allocated with NUMA_NO_NODE. This means /proc/irq/N/node always -reads -1 even when the caller (netback, blkback, netfront, blkfront) -already knows the right node for the IRQ. - -irqbalance treats node=-1 as "no NUMA preference" and distributes -the IRQ across all CPUs for load balance, ignoring affinity_hint. -With irqbalance running, the per-queue NUMA placement we install via -irq_set_affinity_and_hint() gets overwritten almost immediately. - -Add _on_node variants of the four bind helpers Xen front/back ends -use: - - bind_evtchn_to_irq_lateeoi_on_node(evtchn, node) - bind_evtchn_to_irqhandler_lateeoi_on_node(..., node) - bind_interdomain_evtchn_to_irq_lateeoi_on_node(..., node) - bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node(..., node) - -Each passes the caller's node through to the internal chip helpers -and on to a new xen_allocate_irq_dynamic_node(node). The existing -public functions become thin wrappers passing NUMA_NO_NODE, so every -caller that hasn't been updated keeps today's behaviour. - -After this change, /proc/irq/N/node reflects the node the caller -asked for, and irqbalance respects affinity_hint as a NUMA-local -subset rather than treating the IRQ as floating. - -Signed-off-by: Steven Noonan ---- - drivers/xen/events/events_base.c | 86 ++++++++++++++++++++++++++------ - include/xen/events.h | 15 ++++++ - 2 files changed, 85 insertions(+), 16 deletions(-) - -diff --git a/drivers/xen/events/events_base.c b/drivers/xen/events/events_base.c -index 9478fae014e5..6368ff561472 100644 ---- a/drivers/xen/events/events_base.c -+++ b/drivers/xen/events/events_base.c -@@ -28,6 +28,7 @@ - #include - #include - #include -+#include - #include - #include - #include -@@ -729,9 +730,9 @@ static struct irq_info *xen_irq_init(unsigned int irq) - return info; - } - --static struct irq_info *xen_allocate_irq_dynamic(void) -+static struct irq_info *xen_allocate_irq_dynamic_node(int node) - { -- int irq = irq_alloc_desc_from(0, -1); -+ int irq = irq_alloc_desc_from(0, node); - struct irq_info *info = NULL; - - if (irq >= 0) { -@@ -743,6 +744,11 @@ static struct irq_info *xen_allocate_irq_dynamic(void) - return info; - } - -+static struct irq_info *xen_allocate_irq_dynamic(void) -+{ -+ return xen_allocate_irq_dynamic_node(NUMA_NO_NODE); -+} -+ - static struct irq_info *xen_allocate_irq_gsi(unsigned int gsi) - { - int irq; -@@ -1184,7 +1190,8 @@ int xen_pirq_from_irq(unsigned irq) - EXPORT_SYMBOL_GPL(xen_pirq_from_irq); - - static int bind_evtchn_to_irq_chip(evtchn_port_t evtchn, struct irq_chip *chip, -- struct xenbus_device *dev, bool shared) -+ struct xenbus_device *dev, bool shared, -+ int node) - { - int ret = -ENOMEM; - struct irq_info *info; -@@ -1197,7 +1204,7 @@ static int bind_evtchn_to_irq_chip(evtchn_port_t evtchn, struct irq_chip *chip, - info = evtchn_to_info(evtchn); - - if (!info) { -- info = xen_allocate_irq_dynamic(); -+ info = xen_allocate_irq_dynamic_node(node); - if (!info) - goto out; - -@@ -1232,16 +1239,25 @@ static int bind_evtchn_to_irq_chip(evtchn_port_t evtchn, struct irq_chip *chip, - - int bind_evtchn_to_irq(evtchn_port_t evtchn) - { -- return bind_evtchn_to_irq_chip(evtchn, &xen_dynamic_chip, NULL, false); -+ return bind_evtchn_to_irq_chip(evtchn, &xen_dynamic_chip, NULL, false, -+ NUMA_NO_NODE); - } - EXPORT_SYMBOL_GPL(bind_evtchn_to_irq); - - int bind_evtchn_to_irq_lateeoi(evtchn_port_t evtchn) - { -- return bind_evtchn_to_irq_chip(evtchn, &xen_lateeoi_chip, NULL, false); -+ return bind_evtchn_to_irq_chip(evtchn, &xen_lateeoi_chip, NULL, false, -+ NUMA_NO_NODE); - } - EXPORT_SYMBOL_GPL(bind_evtchn_to_irq_lateeoi); - -+int bind_evtchn_to_irq_lateeoi_on_node(evtchn_port_t evtchn, int node) -+{ -+ return bind_evtchn_to_irq_chip(evtchn, &xen_lateeoi_chip, NULL, false, -+ node); -+} -+EXPORT_SYMBOL_GPL(bind_evtchn_to_irq_lateeoi_on_node); -+ - static int bind_ipi_to_irq(unsigned int ipi, unsigned int cpu) - { - struct evtchn_bind_ipi bind_ipi; -@@ -1291,7 +1307,7 @@ static int bind_ipi_to_irq(unsigned int ipi, unsigned int cpu) - static int bind_interdomain_evtchn_to_irq_chip(struct xenbus_device *dev, - evtchn_port_t remote_port, - struct irq_chip *chip, -- bool shared) -+ bool shared, int node) - { - struct evtchn_bind_interdomain bind_interdomain; - int err; -@@ -1303,17 +1319,28 @@ static int bind_interdomain_evtchn_to_irq_chip(struct xenbus_device *dev, - &bind_interdomain); - - return err ? : bind_evtchn_to_irq_chip(bind_interdomain.local_port, -- chip, dev, shared); -+ chip, dev, shared, node); - } - - int bind_interdomain_evtchn_to_irq_lateeoi(struct xenbus_device *dev, - evtchn_port_t remote_port) - { - return bind_interdomain_evtchn_to_irq_chip(dev, remote_port, -- &xen_lateeoi_chip, false); -+ &xen_lateeoi_chip, false, -+ NUMA_NO_NODE); - } - EXPORT_SYMBOL_GPL(bind_interdomain_evtchn_to_irq_lateeoi); - -+int bind_interdomain_evtchn_to_irq_lateeoi_on_node(struct xenbus_device *dev, -+ evtchn_port_t remote_port, -+ int node) -+{ -+ return bind_interdomain_evtchn_to_irq_chip(dev, remote_port, -+ &xen_lateeoi_chip, false, -+ node); -+} -+EXPORT_SYMBOL_GPL(bind_interdomain_evtchn_to_irq_lateeoi_on_node); -+ - static int find_virq(unsigned int virq, unsigned int cpu, evtchn_port_t *evtchn, - bool percpu) - { -@@ -1432,12 +1459,12 @@ static int bind_evtchn_to_irqhandler_chip(evtchn_port_t evtchn, - irq_handler_t handler, - unsigned long irqflags, - const char *devname, void *dev_id, -- struct irq_chip *chip) -+ struct irq_chip *chip, int node) - { - int irq, retval; - - irq = bind_evtchn_to_irq_chip(evtchn, chip, NULL, -- irqflags & IRQF_SHARED); -+ irqflags & IRQF_SHARED, node); - if (irq < 0) - return irq; - retval = request_irq(irq, handler, irqflags, devname, dev_id); -@@ -1456,7 +1483,8 @@ int bind_evtchn_to_irqhandler(evtchn_port_t evtchn, - { - return bind_evtchn_to_irqhandler_chip(evtchn, handler, irqflags, - devname, dev_id, -- &xen_dynamic_chip); -+ &xen_dynamic_chip, -+ NUMA_NO_NODE); - } - EXPORT_SYMBOL_GPL(bind_evtchn_to_irqhandler); - -@@ -1467,19 +1495,34 @@ int bind_evtchn_to_irqhandler_lateeoi(evtchn_port_t evtchn, - { - return bind_evtchn_to_irqhandler_chip(evtchn, handler, irqflags, - devname, dev_id, -- &xen_lateeoi_chip); -+ &xen_lateeoi_chip, -+ NUMA_NO_NODE); - } - EXPORT_SYMBOL_GPL(bind_evtchn_to_irqhandler_lateeoi); - -+int bind_evtchn_to_irqhandler_lateeoi_on_node(evtchn_port_t evtchn, -+ irq_handler_t handler, -+ unsigned long irqflags, -+ const char *devname, -+ void *dev_id, int node) -+{ -+ return bind_evtchn_to_irqhandler_chip(evtchn, handler, irqflags, -+ devname, dev_id, -+ &xen_lateeoi_chip, node); -+} -+EXPORT_SYMBOL_GPL(bind_evtchn_to_irqhandler_lateeoi_on_node); -+ - static int bind_interdomain_evtchn_to_irqhandler_chip( - struct xenbus_device *dev, evtchn_port_t remote_port, - irq_handler_t handler, unsigned long irqflags, -- const char *devname, void *dev_id, struct irq_chip *chip) -+ const char *devname, void *dev_id, struct irq_chip *chip, -+ int node) - { - int irq, retval; - - irq = bind_interdomain_evtchn_to_irq_chip(dev, remote_port, chip, -- irqflags & IRQF_SHARED); -+ irqflags & IRQF_SHARED, -+ node); - if (irq < 0) - return irq; - -@@ -1501,10 +1544,21 @@ int bind_interdomain_evtchn_to_irqhandler_lateeoi(struct xenbus_device *dev, - { - return bind_interdomain_evtchn_to_irqhandler_chip(dev, - remote_port, handler, irqflags, devname, -- dev_id, &xen_lateeoi_chip); -+ dev_id, &xen_lateeoi_chip, NUMA_NO_NODE); - } - EXPORT_SYMBOL_GPL(bind_interdomain_evtchn_to_irqhandler_lateeoi); - -+int bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node( -+ struct xenbus_device *dev, evtchn_port_t remote_port, -+ irq_handler_t handler, unsigned long irqflags, -+ const char *devname, void *dev_id, int node) -+{ -+ return bind_interdomain_evtchn_to_irqhandler_chip(dev, -+ remote_port, handler, irqflags, devname, -+ dev_id, &xen_lateeoi_chip, node); -+} -+EXPORT_SYMBOL_GPL(bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node); -+ - int bind_virq_to_irqhandler(unsigned int virq, unsigned int cpu, - irq_handler_t handler, - unsigned long irqflags, const char *devname, void *dev_id) -diff --git a/include/xen/events.h b/include/xen/events.h -index de5da58a0205..1abc068557b4 100644 ---- a/include/xen/events.h -+++ b/include/xen/events.h -@@ -18,6 +18,7 @@ unsigned xen_evtchn_nr_channels(void); - - int bind_evtchn_to_irq(evtchn_port_t evtchn); - int bind_evtchn_to_irq_lateeoi(evtchn_port_t evtchn); -+int bind_evtchn_to_irq_lateeoi_on_node(evtchn_port_t evtchn, int node); - int bind_evtchn_to_irqhandler(evtchn_port_t evtchn, - irq_handler_t handler, - unsigned long irqflags, const char *devname, -@@ -26,6 +27,10 @@ int bind_evtchn_to_irqhandler_lateeoi(evtchn_port_t evtchn, - irq_handler_t handler, - unsigned long irqflags, const char *devname, - void *dev_id); -+int bind_evtchn_to_irqhandler_lateeoi_on_node(evtchn_port_t evtchn, -+ irq_handler_t handler, -+ unsigned long irqflags, const char *devname, -+ void *dev_id, int node); - int bind_virq_to_irq(unsigned int virq, unsigned int cpu, bool percpu); - int bind_virq_to_irqhandler(unsigned int virq, unsigned int cpu, - irq_handler_t handler, -@@ -39,12 +44,22 @@ int bind_ipi_to_irqhandler(enum ipi_vector ipi, - void *dev_id); - int bind_interdomain_evtchn_to_irq_lateeoi(struct xenbus_device *dev, - evtchn_port_t remote_port); -+int bind_interdomain_evtchn_to_irq_lateeoi_on_node(struct xenbus_device *dev, -+ evtchn_port_t remote_port, -+ int node); - int bind_interdomain_evtchn_to_irqhandler_lateeoi(struct xenbus_device *dev, - evtchn_port_t remote_port, - irq_handler_t handler, - unsigned long irqflags, - const char *devname, - void *dev_id); -+int bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node( -+ struct xenbus_device *dev, -+ evtchn_port_t remote_port, -+ irq_handler_t handler, -+ unsigned long irqflags, -+ const char *devname, -+ void *dev_id, int node); - - /* - * Common unbind function for all event sources. Takes IRQ to unbind from. --- -2.55.0 - diff --git a/patches/0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch b/patches/0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch deleted file mode 100644 index 09489f65..00000000 --- a/patches/0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch +++ /dev/null @@ -1,221 +0,0 @@ -From 28bf8d0a16b76307151e0c35f5da1a20ff1ab9dd Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:52:31 -0700 -Subject: [PATCH 06/12] xen/xenbus: home ring mappings on the foreign frame's - host node - -Xenbus backends in PVH dom0 today have no visibility into which host -NUMA node the foreign frame backing a grant-mapped ring lives on, so -nothing can drive kthread or IRQ placement off of it, and every -backend kthread for every queue piles onto one host node regardless -of the guest's vNUMA layout. - -Make xenbus_map_ring_hvm place its placeholder pages on the foreign -frame's node. Placeholders are first drawn from numa_node_id()'s -pool, because the foreign MFN is not known before the grant_map. -After grant_map succeeds, the first page's host node is resolved via -xen_mfn_to_node and the placeholders are relocated: unmap, free, -re-allocate on the target node, re-map. When the initial allocation -already landed on the right node, the relocate is skipped. After -this, page_to_nid() of every mapped ring page equals the host node of -its foreign MFN by construction. - -Expose the result as xenbus_ring_host_node(dev, vaddr), returning the -Linux node id of the node hosting a mapped ring, or NUMA_NO_NODE when -the information is unavailable. Thanks to the relocation, it is a -thin wrapper around vmalloc_to_page() + page_to_nid(): no hypercall, -no lock, no per-map caching. Backends feed the value to -cpumask_of_node() and kthread_create_on_node(). - -The foreign MFN is sourced from dev_bus_addr in the gnttab map -result. Xen sets that field unconditionally for host_map operations -(see grant_table.c in the hypervisor), so it is reliable in PVH dom0. -Sampling only the first ring page's MFN is sufficient for the common -case where a ring is contiguous on one host node; multi-node -huge-page grants are a future concern. - -The relocate costs one extra grant unmap + map pair per backend -connect (a rare event), paid only when the placeholder pool's node -disagrees with the foreign frame's. A failed unmap during the -relocate marks the pages leaked and fails the whole map, since at -least one grant may still be live against a placeholder that can no -longer be reached safely. - -PV dom0 is not a NUMA-affinity target: PV mappings install foreign -MFNs directly in the PTEs and have no struct page in dom0's mem_map -for the foreign frame, so xenbus_ring_host_node returns NUMA_NO_NODE -there. On hypervisors without XENMEM_get_mfn_pxms the node resolves -to NUMA_NO_NODE at map time and the relocate is skipped entirely. -Backends seeing NUMA_NO_NODE fall back to today's NUMA-oblivious -behaviour. - -Signed-off-by: Steven Noonan ---- - drivers/xen/xenbus/xenbus_client.c | 96 ++++++++++++++++++++++++++++++ - include/xen/xenbus.h | 13 ++++ - 2 files changed, 109 insertions(+) - -diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c -index 2dc874fb5506..34ac3f197b92 100644 ---- a/drivers/xen/xenbus/xenbus_client.c -+++ b/drivers/xen/xenbus/xenbus_client.c -@@ -31,15 +31,18 @@ - */ - - #include -+#include - #include - #include - #include - #include - #include -+#include - #include - #include - #include - #include -+#include - #include - #include - #include -@@ -674,6 +677,7 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - { - struct xenbus_map_node *node = info->node; - int err; -+ int host_node = NUMA_NO_NODE; - void *addr; - bool leaked = false; - unsigned int nr_pages = XENBUS_PAGES(nr_grefs); -@@ -693,6 +697,75 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - if (err) - goto out_free_ballooned_pages; - -+ /* -+ * Xen fills dev_bus_addr with the foreign frame's machine -+ * address on a successful host_map (see grant_table.c in the -+ * hypervisor). Resolve the host node now so we know whether the -+ * placeholders need to be relocated below. -+ */ -+ if (nr_grefs > 0) -+ host_node = xen_mfn_to_node( -+ PFN_DOWN(info->map[0].dev_bus_addr)); -+ -+ /* -+ * Placeholder pages came from numa_node_id()'s pool, which only -+ * matches the foreign frame's node by coincidence. If they -+ * disagree, drop the mapping, return the placeholders, and redo -+ * the map with placeholders drawn from the correct pool. After -+ * this, page_to_nid() of every ring page equals the host node of -+ * its foreign MFN by construction, which keeps grant-mapped pages -+ * truthful to every NUMA-aware code path that consults page_to_nid. -+ * -+ * The cost is one extra grant unmap + map pair per backend -+ * connect (a rare event) and is paid only when the placeholder -+ * pool's node disagrees with the foreign frame. PV mappings and -+ * cases where Xen cannot supply node info skip the dance entirely. -+ */ -+ if (host_node != NUMA_NO_NODE && -+ page_to_nid(node->hvm.pages[0]) != host_node) { -+ int relocate_err; -+ -+ relocate_err = xenbus_unmap_ring(dev, node->handles, nr_grefs, -+ info->addrs); -+ if (relocate_err != GNTST_okay) { -+ /* -+ * Partial unmap: at least one grant may still be -+ * live against a placeholder we can no longer -+ * reach safely. Mark the pages leaked and fail -+ * the whole map. -+ */ -+ leaked = true; -+ err = -EIO; -+ goto out_free_ballooned_pages; -+ } -+ -+ xen_free_unpopulated_pages(nr_pages, node->hvm.pages); -+ -+ err = xen_alloc_unpopulated_pages_node(nr_pages, -+ node->hvm.pages, -+ host_node); -+ if (err) { -+ /* -+ * Pages already gone; clear the array so the -+ * cleanup path does not try to free them again. -+ */ -+ memset(node->hvm.pages, 0, -+ nr_pages * sizeof(*node->hvm.pages)); -+ node->nr_handles = 0; -+ goto out_err; -+ } -+ -+ info->idx = 0; -+ gnttab_foreach_grant(node->hvm.pages, nr_grefs, -+ xenbus_map_ring_setup_grant_hvm, -+ info); -+ -+ err = __xenbus_map_ring(dev, gnt_ref, nr_grefs, node->handles, -+ info, GNTMAP_host_map, &leaked); -+ if (err) -+ goto out_free_ballooned_pages; -+ } -+ - addr = vmap(node->hvm.pages, nr_pages, VM_MAP | VM_IOREMAP, - PAGE_KERNEL); - if (!addr) { -@@ -743,6 +816,29 @@ int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr) - } - EXPORT_SYMBOL_GPL(xenbus_unmap_ring_vfree); - -+int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr) -+{ -+ struct page *page; -+ -+ /* -+ * PV mappings install foreign MFNs directly in the PTEs and have -+ * no struct page in dom0's mem_map for the foreign frame. PVH -+ * dom0 keeps a placeholder struct page (allocated from the -+ * matching per-node pool of xen_alloc_unpopulated_pages_node) -+ * whose page_to_nid() reports the host node of the foreign frame -+ * by construction. -+ */ -+ if (xen_pv_domain()) -+ return NUMA_NO_NODE; -+ -+ page = vmalloc_to_page(vaddr); -+ if (!page) -+ return NUMA_NO_NODE; -+ -+ return page_to_nid(page); -+} -+EXPORT_SYMBOL_GPL(xenbus_ring_host_node); -+ - #ifdef CONFIG_XEN_PV - static int map_ring_apply(pte_t *pte, unsigned long addr, void *data) - { -diff --git a/include/xen/xenbus.h b/include/xen/xenbus.h -index 7dab04cf4a36..18b902bf79ef 100644 ---- a/include/xen/xenbus.h -+++ b/include/xen/xenbus.h -@@ -225,6 +225,19 @@ int xenbus_map_ring_valloc(struct xenbus_device *dev, grant_ref_t *gnt_refs, - - int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr); - -+/* -+ * Return the host NUMA node (Linux node id) of the foreign frame -+ * backing the first page of a mapping previously established by -+ * xenbus_map_ring_valloc(). Returns NUMA_NO_NODE if the hypervisor -+ * cannot provide the information, the mapping is not found, or the -+ * kernel was built without CONFIG_XEN_BACKEND_NUMA_AFFINITY. -+ * -+ * Intended for backends placing their service threads and IRQs on -+ * the node hosting the ring. The value is resolved at map time and -+ * cached on the mapping, so this call is a cheap lookup. -+ */ -+int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr); -+ - int xenbus_alloc_evtchn(struct xenbus_device *dev, evtchn_port_t *port); - int xenbus_free_evtchn(struct xenbus_device *dev, evtchn_port_t port); - --- -2.55.0 - diff --git a/patches/0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch b/patches/0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch deleted file mode 100644 index ea5628f7..00000000 --- a/patches/0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch +++ /dev/null @@ -1,217 +0,0 @@ -From f82bb54fab9f7662be84a7c2c6cff6916ca4e521 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:52:33 -0700 -Subject: [PATCH 07/12] xen/xenbus: add xenbus_setup_ring_node for per-node - ring allocation - -Frontend drivers today route every ring allocation through -xenbus_setup_ring, which calls alloc_pages_exact with no node -preference. The result is that every PV ring -- across all queues -of all multi-queue devices on a guest -- ends up on whichever node -the xenbus watch handler happens to run on, typically a single fixed -value at boot. Multi-queue devices on multi-vnode guests therefore -defeat the dom0 backend's per-queue NUMA affinity work: all of dom0's -backend kthreads cluster on one host node because all of the guest's -rings live on one guest node, which maps to one host node. - -Add a node-aware variant. xenbus_setup_ring_node(dev, gfp, node, ...) -takes a Linux node id and draws the ring pages from that node's buddy -list. xenbus_setup_ring() is now a thin wrapper that passes -NUMA_NO_NODE, preserving existing behaviour for every caller until -they opt in. - -The same-ring locality property is preserved by construction. A -single buddy allocation comes from a single node's free list, so all -pages of one ring remain on one node regardless of which node was -requested. The new variant only changes which node that is. - -alloc_pages_exact_nid is __meminit-restricted and not exported, so -the body cannot just delegate to it. Use alloc_pages_node() (which -is exported and runtime-safe) to get an order-N block on the target -node, then split_page() it so every subpage carries an independent -refcount, and free any tail pages beyond ring_size back to the -allocator. - -Also add xenbus_node_for_queue(index), a helper that rotates over the -set of nodes with online CPUs. Frontend callers feed it a per-queue -or per-ring index to pick the node they pass to -xenbus_setup_ring_node. The natural shape is -cpumask_local_spread(i, NUMA_NO_NODE), but with a NUMA_NO_NODE node -argument that falls back to a linear walk of cpu_online_mask (see -sched_numa_find_nth_cpu) and collapses every queue onto the first -node's CPUs. Going through num_node_state(N_CPU) and -for_each_node_state(node, N_CPU) actually rotates. Living in -xenbus_client.c rather than open-coded in each driver lets future -scsiback / pvcalls-back style frontends pick it up for free. - -Signed-off-by: Steven Noonan ---- - drivers/xen/xenbus/xenbus_client.c | 105 ++++++++++++++++++++++++++--- - include/xen/xenbus.h | 4 ++ - 2 files changed, 99 insertions(+), 10 deletions(-) - -diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c -index 34ac3f197b92..d3108f9fab32 100644 ---- a/drivers/xen/xenbus/xenbus_client.c -+++ b/drivers/xen/xenbus/xenbus_client.c -@@ -31,6 +31,7 @@ - */ - - #include -+#include - #include - #include - #include -@@ -378,33 +379,103 @@ static void xenbus_switch_fatal(struct xenbus_device *dev, int depth, int err, - } - - /* -- * xenbus_setup_ring -+ * xenbus_setup_ring_node - * @dev: xenbus device -+ * @gfp: GFP flags for the allocation -+ * @node: preferred Linux node id for the ring pages, or NUMA_NO_NODE - * @vaddr: pointer to starting virtual address of the ring - * @nr_pages: number of pages to be granted - * @grefs: grant reference array to be filled in - * -- * Allocate physically contiguous pages for a shared ring buffer and grant it -- * to the peer of the given device. The ring buffer is initially filled with -- * zeroes. The virtual address of the ring is stored at @vaddr and the -- * grant references are stored in the @grefs array. In case of error @vaddr -- * will be set to NULL and @grefs will be filled with INVALID_GRANT_REF. -+ * Same contract as xenbus_setup_ring(), but the ring pages are drawn -+ * from @node's buddy free list when possible (subject to fallback when -+ * @node has no available memory). All pages of a single ring come -+ * from one buddy allocation so they remain on a single node by -+ * construction, which is the property frontends rely on to keep -+ * per-queue rings on per-queue nodes. -+ * -+ * The ring buffer is initially filled with zeroes. The virtual address -+ * of the ring is stored at @vaddr and the grant references are stored -+ * in the @grefs array. In case of error @vaddr will be set to NULL and -+ * @grefs will be filled with INVALID_GRANT_REF. - */ --int xenbus_setup_ring(struct xenbus_device *dev, gfp_t gfp, void **vaddr, -- unsigned int nr_pages, grant_ref_t *grefs) -+/* -+ * Pick a Linux node id from the set of nodes with online CPUs, cycling -+ * by @index. Frontends use this to distribute per-queue rings across -+ * guest NUMA nodes so the dom0 backend's per-ring placement lands them -+ * on distinct host nodes. -+ * -+ * cpumask_local_spread(i, NUMA_NO_NODE) is the natural shape this code -+ * wants, but with a NUMA_NO_NODE node argument it falls back to a -+ * straight linear walk of cpu_online_mask (see sched_numa_find_nth_cpu) -+ * which collapses every queue onto the first node's CPUs. This helper -+ * actually rotates over nodes. -+ */ -+int xenbus_node_for_queue(unsigned int index) -+{ -+ unsigned int idx = 0; -+ unsigned int n; -+ int node; -+ -+ n = num_node_state(N_CPU); -+ if (n == 0) -+ return NUMA_NO_NODE; -+ -+ index %= n; -+ for_each_node_state(node, N_CPU) { -+ if (idx == index) -+ return node; -+ idx++; -+ } -+ return NUMA_NO_NODE; -+} -+EXPORT_SYMBOL_GPL(xenbus_node_for_queue); -+ -+int xenbus_setup_ring_node(struct xenbus_device *dev, gfp_t gfp, int node, -+ void **vaddr, unsigned int nr_pages, -+ grant_ref_t *grefs) - { - unsigned long ring_size = nr_pages * XEN_PAGE_SIZE; -+ unsigned int order; -+ unsigned long nr_alloc; -+ struct page *page; - grant_ref_t gref_head; - unsigned int i; - void *addr; - int ret; - -- addr = *vaddr = alloc_pages_exact(ring_size, gfp | __GFP_ZERO); -- if (!*vaddr) { -+ *vaddr = NULL; -+ -+ /* -+ * Mirror the GFP filtering that alloc_pages_exact() does -+ * internally: split_page() below requires a non-compound page -+ * and HIGHMEM is incompatible with the direct virt mapping used -+ * by the grant code. -+ */ -+ gfp &= ~(__GFP_COMP | __GFP_HIGHMEM); -+ -+ order = get_order(ring_size); -+ page = alloc_pages_node(node, gfp | __GFP_ZERO, order); -+ if (!page) { - ret = -ENOMEM; - goto err; - } - -+ /* -+ * alloc_pages_node returns a single order-N block where only -+ * the head is refcounted. split_page makes every subpage -+ * individually refcounted so free_pages_exact() can release the -+ * ring page-by-page. Return any tail pages beyond ring_size to -+ * the allocator immediately. -+ */ -+ split_page(page, order); -+ nr_alloc = 1UL << order; -+ for (i = DIV_ROUND_UP(ring_size, PAGE_SIZE); i < nr_alloc; i++) -+ __free_page(page + i); -+ -+ addr = page_address(page); -+ *vaddr = addr; -+ - ret = gnttab_alloc_grant_references(nr_pages, &gref_head); - if (ret) { - xenbus_dev_fatal(dev, ret, "granting access to %u ring pages", -@@ -438,6 +509,20 @@ int xenbus_setup_ring(struct xenbus_device *dev, gfp_t gfp, void **vaddr, - - return ret; - } -+EXPORT_SYMBOL_GPL(xenbus_setup_ring_node); -+ -+/* -+ * xenbus_setup_ring -+ * -+ * Equivalent to xenbus_setup_ring_node() with no node preference; the -+ * pages come from the current CPU's local node by default GFP policy. -+ */ -+int xenbus_setup_ring(struct xenbus_device *dev, gfp_t gfp, void **vaddr, -+ unsigned int nr_pages, grant_ref_t *grefs) -+{ -+ return xenbus_setup_ring_node(dev, gfp, NUMA_NO_NODE, vaddr, nr_pages, -+ grefs); -+} - EXPORT_SYMBOL_GPL(xenbus_setup_ring); - - /* -diff --git a/include/xen/xenbus.h b/include/xen/xenbus.h -index 18b902bf79ef..8ce096797b86 100644 ---- a/include/xen/xenbus.h -+++ b/include/xen/xenbus.h -@@ -216,6 +216,10 @@ int xenbus_watch_pathfmt(struct xenbus_device *dev, struct xenbus_watch *watch, - const char *pathfmt, ...); - - int xenbus_switch_state(struct xenbus_device *dev, enum xenbus_state new_state); -+int xenbus_node_for_queue(unsigned int index); -+int xenbus_setup_ring_node(struct xenbus_device *dev, gfp_t gfp, int node, -+ void **vaddr, unsigned int nr_pages, -+ grant_ref_t *grefs); - int xenbus_setup_ring(struct xenbus_device *dev, gfp_t gfp, void **vaddr, - unsigned int nr_pages, grant_ref_t *grefs); - void xenbus_teardown_ring(void **vaddr, unsigned int nr_pages, --- -2.55.0 - diff --git a/patches/0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch b/patches/0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch deleted file mode 100644 index 4fa651ee..00000000 --- a/patches/0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch +++ /dev/null @@ -1,258 +0,0 @@ -From 7f3a09f4dac943f542dc8ebaee8c872b1cd33497 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:52:53 -0700 -Subject: [PATCH 08/12] xen-netback: place per-queue kthreads and IRQs near the - ring - -Today both xenvif kthreads (guest-rx and dealloc) and the per-queue -event-channel IRQs run wherever the scheduler happened to place them, -which in PVH dom0 is typically all on one CPU regardless of the -guest's vNUMA layout. When a guest's per-queue ring lives on a -different host node from the kthread, every packet pays cross-node -interconnect cost to walk the ring and to grant-copy payload pages. - -Use xenbus_ring_host_node() to find the host node hosting the -queue's tx ring, then: - - - Create the guest-rx and dealloc kthreads with - kthread_create_on_node() so the task_struct (and kernel stack) is - allocated on the target node from the start. A bare - set_cpus_allowed_ptr() after kthread_run leaves the stack on the - caller's node and continues to pay cross-node cost on every - context switch. - - Pin each kthread to the node's cpumask with - set_cpus_allowed_ptr(). This is a hard pin; an operator can - still override with taskset. No-op when no node info is - available. - - Bind each evtchn IRQ on the ring's node using - bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node() (and the - non-handler variant for the ctrl ring), so the underlying desc is - allocated with the right node attribute and irqbalance treats the - IRQ as NUMA-local rather than floating. For the ctrl ring this - means resolving host_node before the bind instead of after. - - Steer the tx, rx, and ctrl IRQs with irq_set_affinity_and_hint(). - This writes both the actual affinity and the affinity_hint, so a - fresh boot without irqbalance routes IRQs correctly and irqbalance - agrees if it is running. Operator writes to - /proc/irq/N/smp_affinity still win. - - Clear the hint with irq_update_affinity_hint(irq, NULL) right - before each unbind_from_irqhandler() in xenvif_disconnect_queue - and xenvif_disconnect_ctrl. free_irq() warns if the hint is still - set at teardown (kernel/irq/manage.c:1865); destroying a domU - would otherwise WARN in xenwatch context. - -When xenbus_ring_host_node() returns NUMA_NO_NODE (older Xen without -the underlying hypercall, kernel built without -CONFIG_XEN_BACKEND_NUMA_AFFINITY, or a PV mapping), every NUMA-aware -step is skipped and the behaviour is byte-for-byte identical to the -previous code path. - -Signed-off-by: Steven Noonan ---- - drivers/net/xen-netback/interface.c | 102 ++++++++++++++++++++++------ - 1 file changed, 82 insertions(+), 20 deletions(-) - -diff --git a/drivers/net/xen-netback/interface.c b/drivers/net/xen-netback/interface.c -index a0a438881388..798f77595529 100644 ---- a/drivers/net/xen-netback/interface.c -+++ b/drivers/net/xen-netback/interface.c -@@ -31,6 +31,7 @@ - #include "common.h" - - #include -+#include - #include - #include - #include -@@ -638,19 +639,41 @@ int xenvif_connect_ctrl(struct xenvif *vif, grant_ref_t ring_ref, - if (req_prod - rsp_prod > RING_SIZE(&vif->ctrl)) - goto err_unmap; - -- err = bind_interdomain_evtchn_to_irq_lateeoi(xendev, evtchn); -- if (err < 0) -- goto err_unmap; -+ { -+ /* -+ * Resolve the host node before binding the IRQ so the -+ * desc itself is allocated on the right node (which is -+ * what irqbalance reads from /proc/irq/N/node when -+ * deciding affinity). Steer the threaded IRQ handler -+ * toward the same node with irq_set_affinity_and_hint so -+ * a fresh boot without irqbalance also routes correctly. -+ * Operator writes to /proc/irq/N/smp_affinity still win. -+ */ -+ int node = xenbus_ring_host_node(xendev, vif->ctrl.sring); - -- vif->ctrl_irq = err; -+ err = bind_interdomain_evtchn_to_irq_lateeoi_on_node(xendev, -+ evtchn, -+ node); -+ if (err < 0) -+ goto err_unmap; - -- xenvif_init_hash(vif); -+ vif->ctrl_irq = err; - -- err = request_threaded_irq(vif->ctrl_irq, NULL, xenvif_ctrl_irq_fn, -- IRQF_ONESHOT, "xen-netback-ctrl", vif); -- if (err) { -- pr_warn("Could not setup irq handler for %s\n", dev->name); -- goto err_deinit; -+ xenvif_init_hash(vif); -+ -+ err = request_threaded_irq(vif->ctrl_irq, NULL, -+ xenvif_ctrl_irq_fn, -+ IRQF_ONESHOT, -+ "xen-netback-ctrl", vif); -+ if (err) { -+ pr_warn("Could not setup irq handler for %s\n", -+ dev->name); -+ goto err_deinit; -+ } -+ -+ if (node != NUMA_NO_NODE) -+ irq_set_affinity_and_hint(vif->ctrl_irq, -+ cpumask_of_node(node)); - } - - return 0; -@@ -686,6 +709,11 @@ static void xenvif_disconnect_queue(struct xenvif_queue *queue) - } - - if (queue->tx_irq) { -+ /* -+ * free_irq() warns if affinity_hint is still set. Drop the -+ * hint installed at connect time before tearing the IRQ down. -+ */ -+ irq_update_affinity_hint(queue->tx_irq, NULL); - unbind_from_irqhandler(queue->tx_irq, queue); - if (queue->tx_irq == queue->rx_irq) - queue->rx_irq = 0; -@@ -693,6 +721,7 @@ static void xenvif_disconnect_queue(struct xenvif_queue *queue) - } - - if (queue->rx_irq) { -+ irq_update_affinity_hint(queue->rx_irq, NULL); - unbind_from_irqhandler(queue->rx_irq, queue); - queue->rx_irq = 0; - } -@@ -708,6 +737,7 @@ int xenvif_connect_data(struct xenvif_queue *queue, - { - struct xenbus_device *dev = xenvif_to_xenbus_device(queue->vif); - struct task_struct *task; -+ int ring_node; - int err; - - BUG_ON(queue->tx_irq); -@@ -719,6 +749,16 @@ int xenvif_connect_data(struct xenvif_queue *queue, - if (err < 0) - goto err; - -+ /* -+ * Place the per-queue kthreads and IRQs on the host node hosting -+ * the ring page. Most of the per-packet work touches the ring; -+ * keeping the worker local cuts cross-node interconnect traffic. -+ * Returns NUMA_NO_NODE on hypervisors without XENMEM_get_mfn_pxms -+ * or on a kernel built without CONFIG_XEN_BACKEND_NUMA_AFFINITY, -+ * in which case the code below falls back to today's behaviour. -+ */ -+ ring_node = xenbus_ring_host_node(dev, queue->tx.sring); -+ - init_waitqueue_head(&queue->wq); - init_waitqueue_head(&queue->dealloc_wq); - atomic_set(&queue->inflight_packets, 0); -@@ -727,8 +767,14 @@ int xenvif_connect_data(struct xenvif_queue *queue, - - queue->stalled = true; - -- task = kthread_run(xenvif_kthread_guest_rx, queue, -- "%s-guest-rx", queue->name); -+ /* -+ * Split kthread create + wake so the task_struct (and its kernel -+ * stack) is allocated on the target node from the start. A bare -+ * set_cpus_allowed_ptr after kthread_run leaves the stack on the -+ * caller's node. -+ */ -+ task = kthread_create_on_node(xenvif_kthread_guest_rx, queue, -+ ring_node, "%s-guest-rx", queue->name); - if (IS_ERR(task)) - goto kthread_err; - queue->task = task; -@@ -737,43 +783,58 @@ int xenvif_connect_data(struct xenvif_queue *queue, - * if the thread function returns before kthread_stop is called. - */ - get_task_struct(task); -+ if (ring_node != NUMA_NO_NODE) -+ set_cpus_allowed_ptr(task, cpumask_of_node(ring_node)); -+ wake_up_process(task); - -- task = kthread_run(xenvif_dealloc_kthread, queue, -- "%s-dealloc", queue->name); -+ task = kthread_create_on_node(xenvif_dealloc_kthread, queue, -+ ring_node, "%s-dealloc", queue->name); - if (IS_ERR(task)) - goto kthread_err; - queue->dealloc_task = task; -+ if (ring_node != NUMA_NO_NODE) -+ set_cpus_allowed_ptr(task, cpumask_of_node(ring_node)); -+ wake_up_process(task); - - if (tx_evtchn == rx_evtchn) { - /* feature-split-event-channels == 0 */ -- err = bind_interdomain_evtchn_to_irqhandler_lateeoi( -+ err = bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node( - dev, tx_evtchn, xenvif_interrupt, 0, -- queue->name, queue); -+ queue->name, queue, ring_node); - if (err < 0) - goto err; - queue->tx_irq = queue->rx_irq = err; - disable_irq(queue->tx_irq); -+ if (ring_node != NUMA_NO_NODE) -+ irq_set_affinity_and_hint(queue->tx_irq, -+ cpumask_of_node(ring_node)); - } else { - /* feature-split-event-channels == 1 */ - snprintf(queue->tx_irq_name, sizeof(queue->tx_irq_name), - "%s-tx", queue->name); -- err = bind_interdomain_evtchn_to_irqhandler_lateeoi( -+ err = bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node( - dev, tx_evtchn, xenvif_tx_interrupt, 0, -- queue->tx_irq_name, queue); -+ queue->tx_irq_name, queue, ring_node); - if (err < 0) - goto err; - queue->tx_irq = err; - disable_irq(queue->tx_irq); -+ if (ring_node != NUMA_NO_NODE) -+ irq_set_affinity_and_hint(queue->tx_irq, -+ cpumask_of_node(ring_node)); - - snprintf(queue->rx_irq_name, sizeof(queue->rx_irq_name), - "%s-rx", queue->name); -- err = bind_interdomain_evtchn_to_irqhandler_lateeoi( -+ err = bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node( - dev, rx_evtchn, xenvif_rx_interrupt, 0, -- queue->rx_irq_name, queue); -+ queue->rx_irq_name, queue, ring_node); - if (err < 0) - goto err; - queue->rx_irq = err; - disable_irq(queue->rx_irq); -+ if (ring_node != NUMA_NO_NODE) -+ irq_set_affinity_and_hint(queue->rx_irq, -+ cpumask_of_node(ring_node)); - } - - return 0; -@@ -820,6 +881,7 @@ void xenvif_disconnect_ctrl(struct xenvif *vif) - { - if (vif->ctrl_irq) { - xenvif_deinit_hash(vif); -+ irq_update_affinity_hint(vif->ctrl_irq, NULL); - unbind_from_irqhandler(vif->ctrl_irq, vif); - vif->ctrl_irq = 0; - } --- -2.55.0 - diff --git a/patches/0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch b/patches/0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch deleted file mode 100644 index 2c8371c6..00000000 --- a/patches/0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch +++ /dev/null @@ -1,183 +0,0 @@ -From 99386006b8da0d96cbb90fc92f87f7f1893e10f9 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:52:54 -0700 -Subject: [PATCH 09/12] xen-blkback: place per-ring kthread and IRQ near the - ring - -The xenblkd kthread and per-ring event-channel IRQ today run wherever -the scheduler happens to place them, which in PVH dom0 is typically -all on one CPU regardless of the guest's vNUMA layout. When the -guest's per-ring buffer lives on a different host node from the -worker, every request pays cross-node interconnect cost to walk the -ring and to grant-copy payload pages. - -The blkback flow is split across two call sites: xen_blkif_map() maps -the ring and binds the event channel, while xen_update_blkif_status() -spawns the xenblkd kthread later. Stash the ring's host node on the -xen_blkif_ring at map time so the kthread creation site can pick it -up without re-querying. - -At map time: - - Call xenbus_ring_host_node() right after xenbus_map_ring_valloc() - succeeds. Store the result in ring->host_node. - - Bind the IRQ on the ring's node using - bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node() so the - underlying desc is allocated with the right node attribute and - irqbalance treats the IRQ as NUMA-local. - - Steer the bound IRQ with irq_set_affinity_and_hint() so a fresh - boot without irqbalance routes correctly and irqbalance agrees if - it is running. Operator writes still win. - -At kthread creation time: - - Use kthread_create_on_node() with ring->host_node so the - task_struct (and kernel stack) is allocated on the target node - from the start. A bare set_cpus_allowed_ptr() after kthread_run - would leave the stack on the caller's node and pay cross-node - cost on every context switch. - - Hard-pin the kthread to the node's cpumask with - set_cpus_allowed_ptr(). An operator can still override with - taskset. - -At disconnect time: - - Clear the hint with irq_update_affinity_hint(ring->irq, NULL) - right before each unbind_from_irqhandler(). free_irq() warns if - the hint is still set at teardown (kernel/irq/manage.c:1865); - destroying a domU would otherwise WARN in xenwatch context. - -ring->host_node is initialised to NUMA_NO_NODE in xen_blkif_alloc_rings -so any path that misses the map step (or runs on a hypervisor without -XENMEM_get_mfn_pxms, or a kernel without CONFIG_XEN_BACKEND_NUMA_AFFINITY) -falls back to today's NUMA-oblivious behaviour without further checks. - -Signed-off-by: Steven Noonan ---- - drivers/block/xen-blkback/common.h | 7 +++++ - drivers/block/xen-blkback/xenbus.c | 48 ++++++++++++++++++++++++++++-- - 2 files changed, 52 insertions(+), 3 deletions(-) - -diff --git a/drivers/block/xen-blkback/common.h b/drivers/block/xen-blkback/common.h -index b427d54bc120..d670536ebb9e 100644 ---- a/drivers/block/xen-blkback/common.h -+++ b/drivers/block/xen-blkback/common.h -@@ -297,6 +297,13 @@ struct xen_blkif_ring { - /* Thread shutdown wait queue. */ - wait_queue_head_t shutdown_wq; - struct xen_blkif *blkif; -+ -+ /* -+ * Linux node id of the host NUMA node hosting blk_ring, or -+ * NUMA_NO_NODE if unknown. Resolved at xen_blkif_map() time, -+ * consumed when starting the per-ring xenblkd kthread. -+ */ -+ int host_node; - }; - - struct xen_blkif { -diff --git a/drivers/block/xen-blkback/xenbus.c b/drivers/block/xen-blkback/xenbus.c -index 0621878940ae..7ded88e97472 100644 ---- a/drivers/block/xen-blkback/xenbus.c -+++ b/drivers/block/xen-blkback/xenbus.c -@@ -10,6 +10,7 @@ - - #include - #include -+#include - #include - #include - #include -@@ -108,7 +109,17 @@ static void xen_update_blkif_status(struct xen_blkif *blkif) - - for (i = 0; i < blkif->nr_rings; i++) { - ring = &blkif->rings[i]; -- ring->xenblkd = kthread_run(xen_blkif_schedule, ring, "%s-%d", name, i); -+ /* -+ * Allocate the task_struct (including its kernel stack) on -+ * the node hosting the ring so that subsequent kernel-mode -+ * accesses on this thread stay local. set_cpus_allowed_ptr -+ * after kthread_create_on_node hard-pins to that node's CPUs; -+ * an operator may still override with taskset. NUMA_NO_NODE -+ * leaves placement to the default scheduler. -+ */ -+ ring->xenblkd = kthread_create_on_node(xen_blkif_schedule, ring, -+ ring->host_node, -+ "%s-%d", name, i); - if (IS_ERR(ring->xenblkd)) { - err = PTR_ERR(ring->xenblkd); - ring->xenblkd = NULL; -@@ -116,6 +127,10 @@ static void xen_update_blkif_status(struct xen_blkif *blkif) - "start %s-%d xenblkd", name, i); - goto out; - } -+ if (ring->host_node != NUMA_NO_NODE) -+ set_cpus_allowed_ptr(ring->xenblkd, -+ cpumask_of_node(ring->host_node)); -+ wake_up_process(ring->xenblkd); - } - return; - -@@ -150,6 +165,7 @@ static int xen_blkif_alloc_rings(struct xen_blkif *blkif) - init_waitqueue_head(&ring->pending_free_wq); - init_waitqueue_head(&ring->shutdown_wq); - ring->blkif = blkif; -+ ring->host_node = NUMA_NO_NODE; - ring->st_print = jiffies; - ring->active = true; - } -@@ -207,6 +223,15 @@ static int xen_blkif_map(struct xen_blkif_ring *ring, grant_ref_t *gref, - if (err < 0) - return err; - -+ /* -+ * Stash the host node now while the mapping is fresh. The -+ * xenblkd kthread is created later from xen_update_blkif_status() -+ * and consumes this value to place the worker on the node that -+ * owns the ring. NUMA_NO_NODE leaves placement to the default -+ * scheduler. -+ */ -+ ring->host_node = xenbus_ring_host_node(blkif->be->dev, ring->blk_ring); -+ - sring_common = (struct blkif_common_sring *)ring->blk_ring; - rsp_prod = READ_ONCE(sring_common->rsp_prod); - req_prod = READ_ONCE(sring_common->req_prod); -@@ -250,12 +275,23 @@ static int xen_blkif_map(struct xen_blkif_ring *ring, grant_ref_t *gref, - if (req_prod - rsp_prod > size) - goto fail; - -- err = bind_interdomain_evtchn_to_irqhandler_lateeoi(blkif->be->dev, -- evtchn, xen_blkif_be_int, 0, "blkif-backend", ring); -+ err = bind_interdomain_evtchn_to_irqhandler_lateeoi_on_node( -+ blkif->be->dev, evtchn, xen_blkif_be_int, 0, -+ "blkif-backend", ring, ring->host_node); - if (err < 0) - goto fail; - ring->irq = err; - -+ /* -+ * Route the event channel toward the ring's host node. Writes -+ * both the actual affinity (relied on at boot when irqbalance is -+ * absent) and the hint (so irqbalance agrees if it is running). -+ * Operator writes to /proc/irq/N/smp_affinity still win. -+ */ -+ if (ring->host_node != NUMA_NO_NODE) -+ irq_set_affinity_and_hint(ring->irq, -+ cpumask_of_node(ring->host_node)); -+ - return 0; - - fail: -@@ -293,6 +329,12 @@ static int xen_blkif_disconnect(struct xen_blkif *blkif) - } - - if (ring->irq) { -+ /* -+ * free_irq() warns if affinity_hint is still set. -+ * Drop the hint installed at map time before tearing -+ * the IRQ down. -+ */ -+ irq_update_affinity_hint(ring->irq, NULL); - unbind_from_irqhandler(ring->irq, ring); - ring->irq = 0; - } --- -2.55.0 - diff --git a/patches/0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch b/patches/0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch deleted file mode 100644 index 9719fc77..00000000 --- a/patches/0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch +++ /dev/null @@ -1,270 +0,0 @@ -From 6f614fd873050c104e80392784fb0e3f154c6365 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:52:57 -0700 -Subject: [PATCH 10/12] xen-netfront: place per-queue rings on per-queue nodes, - with XPS - -Today every netfront queue allocates its tx and rx rings from -xenbus_setup_ring(), which has no node preference and pulls from the -buddy free list of whichever node the xenbus watch handler is on. -On a multi-queue device, every queue's rings end up on the same -guest node. Combined with vNUMA->host-node mapping that puts each -guest node on its own host node, this funnels all of dom0's -per-queue backend kthreads onto a single host node and defeats the -multi-queue parallelism the dom0 backend is otherwise prepared to -deliver. - -Pick a per-queue node with xenbus_node_for_queue(queue->id) and pass -it to xenbus_setup_ring_node() for both tx and rx rings. Same-ring -locality is preserved by construction (one buddy allocation comes -from one node); different rings of different queues now land on -different nodes on multi-vnode guests. Single-vnode guests -degenerate to node 0 for every queue, identical to the previous -behaviour. - -Thread the same node through setup_netfront_split() and -setup_netfront_single() so the per-queue evtchn IRQs are bound with -bind_evtchn_to_irqhandler_lateeoi_on_node(). The underlying desc is -then allocated with the right node attribute and irqbalance treats -each IRQ as NUMA-local rather than floating. - -Apply irq_set_affinity_and_hint() to each queue's tx/rx IRQ at -connect time using the same per-queue node. NAPI runs in softirq on -the CPU that took the IRQ; landing IRQ + NAPI + ring on one node -keeps the receive path NUMA-local. Sets both actual affinity and -hint so behaviour is correct on guests without irqbalance; operator -writes to /proc/irq/N/smp_affinity still win. - -Install an XPS map for each queue mapping the node's cpumask to that -queue's index. Without this, __netdev_pick_tx falls back to -hash-based queue selection and a sender on any CPU can land on any -queue regardless of where its data wants to live; the ring-placement -work above is then wasted because the actual sender-to-queue -pairing is random. With XPS in place, a sender on a CPU in node N -selects queue N, whose rings are on node N, whose dom0 backend -kthread is on the host node hosting those rings: an end-to-end -NUMA-local TX path with no cross-node payload movement up to the -hypervisor boundary. - -In xennet_disconnect_backend(), clear the hint with -irq_update_affinity_hint(irq, NULL) before each unbind_from_irqhandler -in both the shared- and split-evtchn paths. free_irq() warns if the -hint is still set at teardown (kernel/irq/manage.c:1865); reconnects -would otherwise WARN. - -Operator writes to /proc/sys/net/.../xps_cpus continue to win on -subsequent writes -- this only provides a sensible default. XPS is -also conditional: CONFIG_XPS off makes netif_set_xps_queue a stub, -and an empty cpumask (memory-only NUMA node, possible if unusual on -guests) skips the install to avoid programming an effectively-unusable -map. On reconnect, the new setup_netfront calls overwrite the XPS -map with fresh values; the netdev-scoped map is freed when the -netdev unregisters. - -Signed-off-by: Steven Noonan ---- - drivers/net/xen-netfront.c | 107 ++++++++++++++++++++++++++++++------- - 1 file changed, 88 insertions(+), 19 deletions(-) - -diff --git a/drivers/net/xen-netfront.c b/drivers/net/xen-netfront.c -index a11a0e949400..7c18db3de8d4 100644 ---- a/drivers/net/xen-netfront.c -+++ b/drivers/net/xen-netfront.c -@@ -31,10 +31,12 @@ - - #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt - -+#include - #include - #include - #include - #include -+#include - #include - #include - #include -@@ -1824,9 +1826,17 @@ static void xennet_disconnect_backend(struct netfront_info *info) - - timer_delete_sync(&queue->rx_refill_timer); - -- if (queue->tx_irq && (queue->tx_irq == queue->rx_irq)) -+ /* -+ * free_irq() warns if affinity_hint is still set. Drop the -+ * hint installed at connect time before tearing the IRQ down. -+ */ -+ if (queue->tx_irq && (queue->tx_irq == queue->rx_irq)) { -+ irq_update_affinity_hint(queue->tx_irq, NULL); - unbind_from_irqhandler(queue->tx_irq, queue); -+ } - if (queue->tx_irq && (queue->tx_irq != queue->rx_irq)) { -+ irq_update_affinity_hint(queue->tx_irq, NULL); -+ irq_update_affinity_hint(queue->rx_irq, NULL); - unbind_from_irqhandler(queue->tx_irq, queue); - unbind_from_irqhandler(queue->rx_irq, queue); - } -@@ -1902,7 +1912,7 @@ static int xen_net_read_mac(struct xenbus_device *dev, u8 mac[]) - return 0; - } - --static int setup_netfront_single(struct netfront_queue *queue) -+static int setup_netfront_single(struct netfront_queue *queue, int node) - { - int err; - -@@ -1910,10 +1920,10 @@ static int setup_netfront_single(struct netfront_queue *queue) - if (err < 0) - goto fail; - -- err = bind_evtchn_to_irqhandler_lateeoi(queue->tx_evtchn, -- xennet_interrupt, 0, -- queue->info->netdev->name, -- queue); -+ err = bind_evtchn_to_irqhandler_lateeoi_on_node(queue->tx_evtchn, -+ xennet_interrupt, 0, -+ queue->info->netdev->name, -+ queue, node); - if (err < 0) - goto bind_fail; - queue->rx_evtchn = queue->tx_evtchn; -@@ -1928,7 +1938,7 @@ static int setup_netfront_single(struct netfront_queue *queue) - return err; - } - --static int setup_netfront_split(struct netfront_queue *queue) -+static int setup_netfront_split(struct netfront_queue *queue, int node) - { - int err; - -@@ -1941,18 +1951,20 @@ static int setup_netfront_split(struct netfront_queue *queue) - - snprintf(queue->tx_irq_name, sizeof(queue->tx_irq_name), - "%s-tx", queue->name); -- err = bind_evtchn_to_irqhandler_lateeoi(queue->tx_evtchn, -- xennet_tx_interrupt, 0, -- queue->tx_irq_name, queue); -+ err = bind_evtchn_to_irqhandler_lateeoi_on_node(queue->tx_evtchn, -+ xennet_tx_interrupt, 0, -+ queue->tx_irq_name, -+ queue, node); - if (err < 0) - goto bind_tx_fail; - queue->tx_irq = err; - - snprintf(queue->rx_irq_name, sizeof(queue->rx_irq_name), - "%s-rx", queue->name); -- err = bind_evtchn_to_irqhandler_lateeoi(queue->rx_evtchn, -- xennet_rx_interrupt, 0, -- queue->rx_irq_name, queue); -+ err = bind_evtchn_to_irqhandler_lateeoi_on_node(queue->rx_evtchn, -+ xennet_rx_interrupt, 0, -+ queue->rx_irq_name, -+ queue, node); - if (err < 0) - goto bind_rx_fail; - queue->rx_irq = err; -@@ -1977,6 +1989,7 @@ static int setup_netfront(struct xenbus_device *dev, - { - struct xen_netif_tx_sring *txs; - struct xen_netif_rx_sring *rxs; -+ int node; - int err; - - queue->tx_ring_ref = INVALID_GRANT_REF; -@@ -1984,32 +1997,88 @@ static int setup_netfront(struct xenbus_device *dev, - queue->rx.sring = NULL; - queue->tx.sring = NULL; - -- err = xenbus_setup_ring(dev, GFP_NOIO | __GFP_HIGH, (void **)&txs, -- 1, &queue->tx_ring_ref); -+ /* -+ * Distribute queues across guest NUMA nodes by rotating over -+ * nodes-with-CPUs. On a single-vnode guest every queue lands -+ * on node 0 and behaviour matches the legacy default. On a -+ * multi-vnode guest, queues spread across nodes and pair up -+ * naturally with the dom0 backend's per-queue node-affinity -+ * placement. -+ */ -+ node = xenbus_node_for_queue(queue->id); -+ -+ err = xenbus_setup_ring_node(dev, GFP_NOIO | __GFP_HIGH, node, -+ (void **)&txs, 1, &queue->tx_ring_ref); - if (err) - goto fail; - - XEN_FRONT_RING_INIT(&queue->tx, txs, XEN_PAGE_SIZE); - -- err = xenbus_setup_ring(dev, GFP_NOIO | __GFP_HIGH, (void **)&rxs, -- 1, &queue->rx_ring_ref); -+ err = xenbus_setup_ring_node(dev, GFP_NOIO | __GFP_HIGH, node, -+ (void **)&rxs, 1, &queue->rx_ring_ref); - if (err) - goto fail; - - XEN_FRONT_RING_INIT(&queue->rx, rxs, XEN_PAGE_SIZE); - - if (feature_split_evtchn) -- err = setup_netfront_split(queue); -+ err = setup_netfront_split(queue, node); - /* setup single event channel if - * a) feature-split-event-channels == 0 - * b) feature-split-event-channels == 1 but failed to setup - */ - if (!feature_split_evtchn || err) -- err = setup_netfront_single(queue); -+ err = setup_netfront_single(queue, node); - - if (err) - goto fail; - -+ /* -+ * Route each per-queue evtchn IRQ toward the same node the ring -+ * lives on. NAPI runs in softirq on the CPU that took the IRQ; -+ * landing IRQ + NAPI + ring on one node keeps the receive path -+ * NUMA-local. Sets both actual affinity and hint so behaviour -+ * is correct on guests without irqbalance. Operator writes to -+ * /proc/irq/N/smp_affinity continue to win. -+ */ -+ if (node != NUMA_NO_NODE) { -+ const struct cpumask *mask = cpumask_of_node(node); -+ -+ if (!cpumask_empty(mask)) { -+ irq_set_affinity_and_hint(queue->tx_irq, mask); -+ if (queue->rx_irq != queue->tx_irq) -+ irq_set_affinity_and_hint(queue->rx_irq, mask); -+ } -+ } -+ -+ /* -+ * Steer senders toward this queue based on the same node the -+ * rings live on. __netdev_pick_tx consults the XPS map first; -+ * a sender on a CPU in `node` will pick `queue->id`, whose -+ * rings are on `node`, and dom0's matching backend kthread is -+ * pinned to the host node that hosts those rings. Without -+ * this, the kernel's default hash-based queue selection lets a -+ * sender on any node land on any queue, defeating the per-queue -+ * NUMA-locality story. An operator write to xps_cpus -+ * overrides on subsequent writes. -+ * -+ * netif_set_xps_queue silently returns 0 if CONFIG_XPS is off; -+ * an empty cpumask (e.g. memory-only NUMA node) is skipped to -+ * avoid programming an effectively-unusable map. -+ */ -+ if (node != NUMA_NO_NODE) { -+ const struct cpumask *mask = cpumask_of_node(node); -+ -+ if (!cpumask_empty(mask)) { -+ int xps_err = netif_set_xps_queue(queue->info->netdev, -+ mask, queue->id); -+ if (xps_err) -+ netdev_warn(queue->info->netdev, -+ "XPS setup failed for queue %u: %d\n", -+ queue->id, xps_err); -+ } -+ } -+ - return 0; - - fail: --- -2.55.0 - diff --git a/patches/0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch b/patches/0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch deleted file mode 100644 index 8fa99145..00000000 --- a/patches/0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch +++ /dev/null @@ -1,158 +0,0 @@ -From 3f4b79bd1c1e8fac01ae80121a2eec2b9b05d81d Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:52:59 -0700 -Subject: [PATCH 11/12] xen-blkfront: place per-ring buffers on per-hctx nodes - -Today every blkfront ring (one per hctx in multi-queue mode) is -allocated from xenbus_setup_ring() with no node preference, so all -of a multi-queue blkfront's rings end up on whichever node the -xenbus watch handler runs on. Combined with vNUMA->host-node -mapping, this funnels every dom0 backend xenblkd kthread onto a -single host node and defeats multi-queue parallelism. - -Pick a per-ring node with xenbus_node_for_queue(ring_idx) and pass it -to xenbus_setup_ring_node(). Different rings now land on different -guest nodes on multi-vnode guests; same-ring locality is preserved -by the underlying buddy allocation. - -The ring's index is recovered from its byte offset within -info->rinfo because struct blkfront_ring_info has a flex array -trailer and sizeof() is not the stride; the same arithmetic appears -in get_rinfo() and for_each_rinfo(). - -Bind the ring's evtchn IRQ on the ring's node using -bind_evtchn_to_irqhandler_lateeoi_on_node(). The desc is then -allocated with the right node attribute and irqbalance treats the -IRQ as NUMA-local. - -Apply irq_set_affinity_and_hint() to the per-ring IRQ using the same -node. The IRQ fires on a CPU in the ring's node; blk-mq routes the -completion onward to the original submitter via the request_irq -machinery, so the wake-up edge stays on the right node before that -handoff. Sets both actual affinity and hint; operator writes to -/proc/irq/N/smp_affinity continue to win. - -No XPS-equivalent steering is needed on the block side. blk-mq's -existing hctx-to-CPU map already routes submissions: a process -running on CPU C submits via the hctx blk-mq mapped to C, and our -ring node for that hctx is derived from the same node-rotation -helper. The submitter is on the same node as the ring by -construction. - -In blkif_free_ring(), clear the hint with -irq_update_affinity_hint(rinfo->irq, NULL) before -unbind_from_irqhandler(). free_irq() warns if the hint is still set -at teardown (kernel/irq/manage.c:1865); reconnects would otherwise -WARN. - -Single-vnode guests are unaffected: every ring's node resolves to -node 0 and behaviour matches the previous default. - -Signed-off-by: Steven Noonan ---- - drivers/block/xen-blkfront.c | 53 ++++++++++++++++++++++++++++++++---- - 1 file changed, 48 insertions(+), 5 deletions(-) - -diff --git a/drivers/block/xen-blkfront.c b/drivers/block/xen-blkfront.c -index 04fc6b552c04..8414dc737157 100644 ---- a/drivers/block/xen-blkfront.c -+++ b/drivers/block/xen-blkfront.c -@@ -35,12 +35,14 @@ - * IN THE SOFTWARE. - */ - -+#include - #include - #include - #include - #include - #include - #include -+#include - #include - #include - #include -@@ -1293,8 +1295,14 @@ static void blkif_free_ring(struct blkfront_ring_info *rinfo) - xenbus_teardown_ring((void **)&rinfo->ring.sring, info->nr_ring_pages, - rinfo->ring_ref); - -- if (rinfo->irq) -+ if (rinfo->irq) { -+ /* -+ * free_irq() warns if affinity_hint is still set. Drop the -+ * hint installed at setup time before tearing the IRQ down. -+ */ -+ irq_update_affinity_hint(rinfo->irq, NULL); - unbind_from_irqhandler(rinfo->irq, rinfo); -+ } - rinfo->evtchn = rinfo->irq = 0; - } - -@@ -1684,9 +1692,29 @@ static int setup_blkring(struct xenbus_device *dev, - int err; - struct blkfront_info *info = rinfo->dev_info; - unsigned long ring_size = info->nr_ring_pages * XEN_PAGE_SIZE; -+ unsigned int ring_idx; -+ int node; -+ -+ /* -+ * Recover the ring index from its slot in info->rinfo. The same -+ * arithmetic is used by get_rinfo() and for_each_rinfo() (the -+ * struct has a flex array so sizeof() is not the stride). -+ */ -+ ring_idx = ((unsigned long)rinfo - (unsigned long)info->rinfo) / -+ info->rinfo_size; -+ -+ /* -+ * Distribute rings across guest NUMA nodes by rotating over -+ * nodes-with-CPUs. blk-mq's default hctx-to-CPU map is also -+ * NUMA-balanced (blk_mq_map_queues uses NUMA-aware distribution -+ * when topology is present), so a submitter on a node-N CPU -+ * lands on the hctx whose ring is on node N. No XPS-equivalent -+ * steering needed on the block side -- blk-mq already does it. -+ */ -+ node = xenbus_node_for_queue(ring_idx); - -- err = xenbus_setup_ring(dev, GFP_NOIO, (void **)&sring, -- info->nr_ring_pages, rinfo->ring_ref); -+ err = xenbus_setup_ring_node(dev, GFP_NOIO, node, (void **)&sring, -+ info->nr_ring_pages, rinfo->ring_ref); - if (err) - goto fail; - -@@ -1696,8 +1724,9 @@ static int setup_blkring(struct xenbus_device *dev, - if (err) - goto fail; - -- err = bind_evtchn_to_irqhandler_lateeoi(rinfo->evtchn, blkif_interrupt, -- 0, "blkif", rinfo); -+ err = bind_evtchn_to_irqhandler_lateeoi_on_node(rinfo->evtchn, -+ blkif_interrupt, 0, -+ "blkif", rinfo, node); - if (err <= 0) { - xenbus_dev_fatal(dev, err, - "bind_evtchn_to_irqhandler failed"); -@@ -1705,6 +1734,20 @@ static int setup_blkring(struct xenbus_device *dev, - } - rinfo->irq = err; - -+ /* -+ * Route the ring's evtchn IRQ toward the same node the ring -+ * lives on. blk-mq completes requests on the submitting CPU -+ * via the request_irq path; firing the IRQ on the ring's node -+ * keeps the wake-up on the right node before blk-mq routes the -+ * completion onward. Sets both actual affinity and hint. -+ */ -+ if (node != NUMA_NO_NODE) { -+ const struct cpumask *mask = cpumask_of_node(node); -+ -+ if (!cpumask_empty(mask)) -+ irq_set_affinity_and_hint(rinfo->irq, mask); -+ } -+ - return 0; - fail: - blkif_free(info, 0); --- -2.55.0 - diff --git a/patches/0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch b/patches/0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch deleted file mode 100644 index f68ddc30..00000000 --- a/patches/0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch +++ /dev/null @@ -1,198 +0,0 @@ -From 1ba42d8e6f950e6946bee2e2f971c2ffdaa89317 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Mon, 20 Jul 2026 13:53:00 -0700 -Subject: [PATCH 12/12] xen/gntdev: home grant-map placeholder pages on the - foreign frame's node - -On an auto-translated dom0, gntdev backs each userspace grant mapping -with placeholder pages drawn from the unpopulated-page pool of the -mapping CPU's local node. page_to_nid() of such a page therefore -reflects where the mapper happened to run, not where the foreign frame -actually lives, and userspace has no way at all to learn the frame's -home: gntdev never surfaces dev_bus_addr, and /proc/self/pagemap only -yields the placeholder's dom0 pseudo-physical frame. - -Do for gntdev what xenbus_map_ring_hvm already does for kernel ring -mappings. After the first successful gnttab_map_refs -- and before -the pages become visible outside the map: gntdev_mmap() only inserts -them into the VMA afterwards, and map->in_use limits a map to a single -mmap -- resolve the first frame's host node via xen_mfn_to_node. If -the placeholders live elsewhere, allocate replacements from the -matching pool, unmap, and remap onto them. The frames of one map -overwhelmingly share a node (they are rings or buffers the granting -guest allocated together), so following the first frame mirrors the -xenbus precedent. DMA-allocated backing (CONFIG_XEN_GRANT_DMA_ALLOC) -is skipped: its physical placement is part of its contract. - -The ordering makes failure handling one-directional. Replacement -pages are allocated before anything moves, so allocation failure -- -like an unresolvable node, a hint-free hypervisor, or a domU caller -(xen_mfn_to_node answers only in the hardware domain) -- leaves the -original mapping untouched. Once the old mapping has been torn down, -a failure poisons the map's statuses and handles so the caller's -accounting loop reports the map as wholly failed and teardown does not -unmap a second time; should the unmap itself fail, the old pages may -still carry live foreign mappings, so they are deliberately leaked -rather than returned to the allocator, exactly as the xenbus relocate -leaks placeholders it can no longer reach safely. - -The payoff is that the node information becomes visible to userspace -through an existing interface: get_mempolicy(MPOL_F_ADDR | MPOL_F_NODE) -on a gntdev-mapped address returns page_to_nid of the placeholder -(gntdev VMAs are VM_MIXEDMAP with normal PTEs, which the GUP slow path -resolves), which now equals the foreign frame's node. A userspace -paravirtual backend can map a guest's ring, ask the kernel which node -that ring lives on, and bind its memory and I/O threads to match -- -the userspace analogue of the netback/blkback per-queue placement. -move_pages(2) and /proc/self/numa_maps still ignore these mappings; -both explicitly skip ZONE_DEVICE pages. - -Signed-off-by: Steven Noonan ---- - drivers/xen/gntdev.c | 123 +++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 123 insertions(+) - -diff --git a/drivers/xen/gntdev.c b/drivers/xen/gntdev.c -index 2c960f187f7c..31da3c9641f1 100644 ---- a/drivers/xen/gntdev.c -+++ b/drivers/xen/gntdev.c -@@ -327,6 +327,119 @@ static int find_grant_ptes(pte_t *pte, unsigned long addr, void *data) - return 0; - } - -+/* -+ * Relocate a freshly mapped map's placeholder pages onto the NUMA node -+ * hosting the foreign frames, so that page_to_nid() of a grant-mapped -+ * page reports the frame's true home -- the same dance -+ * xenbus_map_ring_hvm() performs for kernel ring mappings. Userspace -+ * can then recover the node of a mapping it created with -+ * get_mempolicy(MPOL_F_ADDR | MPOL_F_NODE) and place its I/O threads -+ * and buffers accordingly. -+ * -+ * Runs after the first successful gnttab_map_refs() and before the -+ * pages are visible outside the map (gntdev_mmap() inserts them into -+ * the VMA only after gntdev_map_grant_pages() returns, and map->in_use -+ * limits a map to a single mmap), so the unmap/remap below cannot race -+ * a user access. -+ * -+ * Best-effort while nothing has moved: any failure to learn or improve -+ * the placement leaves the original mapping untouched and returns 0. A -+ * failure while moving the mapping poisons the map and returns an -+ * error, since a half-relocated map must not be exposed. -+ */ -+static int gntdev_relocate_map(struct gntdev_grant_map *map) -+{ -+ struct page **pages; -+ int node, i, err; -+ -+#ifdef CONFIG_XEN_GRANT_DMA_ALLOC -+ /* DMA-allocated backing must keep its physical placement. */ -+ if (map->dma_vaddr) -+ return 0; -+#endif -+ -+ /* Only relocate a fully successful map. */ -+ for (i = 0; i < map->count; i++) -+ if (map->map_ops[i].status != GNTST_okay) -+ return 0; -+ -+ /* -+ * Xen filled dev_bus_addr with the foreign frame's machine -+ * address on the successful host_map. The frames of one map -+ * overwhelmingly share a node (they are rings or buffers the -+ * granting guest allocated together), so follow the first frame, -+ * as the xenbus ring path does. -+ */ -+ node = xen_mfn_to_node(PFN_DOWN(map->map_ops[0].dev_bus_addr)); -+ if (node == NUMA_NO_NODE || node == page_to_nid(map->pages[0])) -+ return 0; -+ -+ pages = kvcalloc(map->count, sizeof(pages[0]), GFP_KERNEL); -+ if (!pages) -+ return 0; -+ if (gnttab_alloc_pages_node(map->count, pages, node)) { -+ kvfree(pages); -+ return 0; -+ } -+ -+ for (i = 0; i < map->count; i++) { -+ map->unmap_ops[i].handle = map->map_ops[i].handle; -+ if (map->flags & GNTMAP_device_map) -+ map->unmap_ops[i].dev_bus_addr = -+ map->map_ops[i].dev_bus_addr; -+ } -+ err = gnttab_unmap_refs(map->unmap_ops, map->kunmap_ops, map->pages, -+ map->count); -+ if (err) { -+ /* -+ * The old placeholders may still carry live foreign -+ * mappings; freeing them would hand foreign-mapped frames -+ * back to the allocator. Swap in the clean replacements -+ * so teardown frees those instead, deliberately leak the -+ * old pages, and fail the map. -+ */ -+ pr_err("relocate unmap failed (%d); leaking %d pages\n", -+ err, map->count); -+ memcpy(map->pages, pages, map->count * sizeof(pages[0])); -+ kvfree(pages); -+ goto poison; -+ } -+ -+ gnttab_free_pages(map->count, map->pages); -+ memcpy(map->pages, pages, map->count * sizeof(pages[0])); -+ kvfree(pages); -+ -+ for (i = 0; i < map->count; i++) { -+ unsigned long addr = (unsigned long) -+ pfn_to_kaddr(page_to_pfn(map->pages[i])); -+ gnttab_set_map_op(&map->map_ops[i], addr, map->flags, -+ map->grants[i].ref, map->grants[i].domid); -+ gnttab_set_unmap_op(&map->unmap_ops[i], addr, map->flags, -+ INVALID_GRANT_HANDLE); -+ } -+ err = gnttab_map_refs(map->map_ops, map->kmap_ops, map->pages, -+ map->count); -+ if (err) { -+ pr_err("relocate remap on node %d failed (%d)\n", node, err); -+ goto poison; -+ } -+ -+ return 0; -+ -+poison: -+ /* -+ * Present the map as wholly failed: no handle survives for the -+ * caller's status loop to account, or for teardown to unmap a -+ * second time. -+ */ -+ for (i = 0; i < map->count; i++) { -+ map->map_ops[i].status = GNTST_general_error; -+ map->map_ops[i].handle = INVALID_GRANT_HANDLE; -+ map->unmap_ops[i].handle = INVALID_GRANT_HANDLE; -+ } -+ return err; -+} -+ - int gntdev_map_grant_pages(struct gntdev_grant_map *map) - { - size_t alloced = 0; -@@ -377,6 +490,16 @@ int gntdev_map_grant_pages(struct gntdev_grant_map *map) - err = gnttab_map_refs(map->map_ops, map->kmap_ops, map->pages, - map->count); - -+ /* -+ * Auto-translated placeholders came from the local node's pool, -+ * which matches the foreign frames' node only by coincidence; -+ * move them to the frames' real home while nothing can observe -+ * the map. PV installs foreign MFNs directly in the PTEs and has -+ * no dom0 struct page to relocate. -+ */ -+ if (!err && !xen_pv_domain()) -+ err = gntdev_relocate_map(map); -+ - for (i = 0; i < map->count; i++) { - if (map->map_ops[i].status == GNTST_okay) { - map->unmap_ops[i].handle = map->map_ops[i].handle; --- -2.55.0 - diff --git a/patches/9pfs-xen-hotplug-1.patch b/patches/9pfs-xen-hotplug-1.patch deleted file mode 100644 index fd6c65f6..00000000 --- a/patches/9pfs-xen-hotplug-1.patch +++ /dev/null @@ -1,43 +0,0 @@ -commit 9fe2b8c365d7bd2ad447ff1abe21016a155185e4 -Author: Alex Zenla -Date: Tue Nov 19 10:40:41 2024 -0500 - - 9p/xen: fix init sequence - - The 9pfs xen backend has historically been used mostly at boot-time. - We've been hotplugging 9pfs devices and noticed a large amount of - mount hangs. It appears that the 9pfs xen driver was initializing - itself multiple times, causing the frontend and backend to diagree. - This caused the backend to be listening on a channel that the frontend - was not sending on, resulting in stalled processing. It was sometimes - possible for the first message ring to connect properly, but additional - rings might get into the bad state. This change fixes this by ensuring - that the driver can only initialize the frontend once. - - Signed-off-by: Alex Zenla - -diff --git a/net/9p/trans_xen.c b/net/9p/trans_xen.c -index dfdbe1ca5338..0304e8a1616d 100644 ---- a/net/9p/trans_xen.c -+++ b/net/9p/trans_xen.c -@@ -465,6 +465,7 @@ static int xen_9pfs_front_init(struct xenbus_device *dev) - goto error; - } - -+ xenbus_switch_state(dev, XenbusStateInitialised); - return 0; - - error_xenbus: -@@ -512,8 +513,10 @@ static void xen_9pfs_front_changed(struct xenbus_device *dev, - break; - - case XenbusStateInitWait: -- if (!xen_9pfs_front_init(dev)) -- xenbus_switch_state(dev, XenbusStateInitialised); -+ if (dev->state != XenbusStateInitialising) -+ break; -+ -+ xen_9pfs_front_init(dev); - break; - - case XenbusStateConnected: diff --git a/patches/9pfs-xen-hotplug-2.patch b/patches/9pfs-xen-hotplug-2.patch deleted file mode 100644 index 6c7f5625..00000000 --- a/patches/9pfs-xen-hotplug-2.patch +++ /dev/null @@ -1,24 +0,0 @@ -commit ad1addb5e0c87523196c4c46b11a14104d0ea5fa -Author: Alex Zenla -Date: Thu Nov 21 16:08:21 2024 -0500 - - 9p/xen: fix irq unbind - - 9pfs xen transport passed the device rather than the ring handle - into the unbind call for xen irqs. Fix this by passing the ring - instead. - -diff --git a/net/9p/trans_xen.c b/net/9p/trans_xen.c -index 0304e8a1616d..cb2c301445f9 100644 ---- a/net/9p/trans_xen.c -+++ b/net/9p/trans_xen.c -@@ -286,7 +286,8 @@ static void xen_9pfs_front_free(struct xen_9pfs_front_priv *priv) - if (!priv->rings[i].intf) - break; - if (priv->rings[i].irq > 0) -- unbind_from_irqhandler(priv->rings[i].irq, priv->dev); -+ unbind_from_irqhandler(priv->rings[i].irq, ring); -+ priv->rings[i].evtchn = priv->rings[i].irq = 0; - if (priv->rings[i].data.in) { - for (j = 0; - j < (1 << priv->rings[i].intf->ring_order); diff --git a/patches/9pfs-xen-multi-attach-6.18.patch b/patches/9pfs-xen-multi-attach-6.18.patch deleted file mode 100644 index 94e1569d..00000000 --- a/patches/9pfs-xen-multi-attach-6.18.patch +++ /dev/null @@ -1,368 +0,0 @@ -From a5a16b585cb5925fdd6c2f13db2c0b8896a29697 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Tue, 14 Jul 2026 23:27:39 -0700 -Subject: [PATCH] 9p/xen: share one frontend across mounts and advertise it - (6.18) - -Backport of the multi-attach change to the pre-fs_context 9p mount API -used by 6.18.x. Let the 9p core share one refcounted p9_client across all -mounts of an endpoint (opt-in via p9_trans_module.share_client, set for -xen), so several mounts share a frontend, each attaching with its own -aname to get an independent tree and superblock. - -The endpoint is selected by a tag= mount option (parsed from the options -string in the old API) so the mount source can differ per mount (distinct -/proc/mounts device names), and the capability is advertised to userspace -as edera_multi_attach_v1 in /sys/fs/9p/features. - -Signed-off-by: Alex Zenla ---- - fs/9p/v9fs.c | 20 ++++++ - include/net/9p/client.h | 10 +++ - include/net/9p/transport.h | 8 +++ - net/9p/client.c | 125 +++++++++++++++++++++++++++++++++++++ - net/9p/trans_xen.c | 40 +++++++++++- - 5 files changed, 200 insertions(+), 3 deletions(-) - -diff --git a/fs/9p/v9fs.c b/fs/9p/v9fs.c -index bde3ffb0e319..daaca8117f53 100644 ---- a/fs/9p/v9fs.c -+++ b/fs/9p/v9fs.c -@@ -578,10 +578,30 @@ static ssize_t caches_show(struct kobject *kobj, - static struct kobj_attribute v9fs_attr_cache = __ATTR_RO(caches); - #endif /* CONFIG_9P_FSCACHE */ - -+/* -+ * Capability tokens for userspace to probe, one per line. -+ * -+ * "edera_multi_attach_v1": a transport that sets p9_trans_module.share_client -+ * (e.g. Xen 9pfs) can back several mounts of a single endpoint, each attaching -+ * with its own aname. Userspace can test for this token before mounting subtrees -+ * of one frontend as independent superblocks (distinct st_dev) instead of -+ * bind-mounting from a single mount. The token is vendor-namespaced and -+ * versioned so it never aliases an unrelated upstream feature name, and a future -+ * behavior change can advertise "_v2" instead. -+ */ -+static ssize_t features_show(struct kobject *kobj, -+ struct kobj_attribute *attr, char *buf) -+{ -+ return sysfs_emit(buf, "edera_multi_attach_v1\n"); -+} -+ -+static struct kobj_attribute v9fs_attr_features = __ATTR_RO(features); -+ - static struct attribute *v9fs_attrs[] = { - #ifdef CONFIG_9P_FSCACHE - &v9fs_attr_cache.attr, - #endif -+ &v9fs_attr_features.attr, - NULL, - }; - -diff --git a/include/net/9p/client.h b/include/net/9p/client.h -index 4f785098c67a..763cad1359a0 100644 ---- a/include/net/9p/client.h -+++ b/include/net/9p/client.h -@@ -123,6 +123,16 @@ struct p9_client { - struct idr fids; - struct idr reqs; - -+ /* Client sharing, for transports with p9_trans_module.share_client set: -+ * all mounts of one endpoint reference a single client. @refcount counts -+ * those mounts; @shared_list links the client into the shared-client -+ * registry, keyed by @shared_key (a copy of the endpoint tag/source). -+ * @shared_key is NULL for ordinary, unshared clients. -+ */ -+ refcount_t refcount; -+ struct list_head shared_list; -+ char *shared_key; -+ - char name[__NEW_UTS_LEN + 1]; - }; - -diff --git a/include/net/9p/transport.h b/include/net/9p/transport.h -index 766ec07c9599..0bc000853b58 100644 ---- a/include/net/9p/transport.h -+++ b/include/net/9p/transport.h -@@ -24,6 +24,13 @@ - * we're less flexible when choosing the response message - * size in this case - * @def: set if this transport should be considered the default -+ * @share_client: set if one transport endpoint (e.g. a Xen 9pfs frontend, -+ * keyed by its tag) can back more than one mount. Such an -+ * endpoint cannot multiplex several p9_clients, so when this is -+ * set the 9p core hands every mount of the same endpoint a -+ * single, refcounted p9_client instead of one per mount. Each -+ * mount still issues its own Tattach (with its own aname), so it -+ * gets an independent tree and superblock over the shared client. - * @create: member function to create a new connection on this transport - * @close: member function to discard a connection on this transport - * @request: member function to issue a request to the transport -@@ -44,6 +51,7 @@ struct p9_trans_module { - int maxsize; /* max message size of transport */ - bool pooled_rbuffers; - int def; /* this transport should be default */ -+ bool share_client; /* one endpoint may back many mounts */ - struct module *owner; - int (*create)(struct p9_client *client, - const char *devname, char *args); -diff --git a/net/9p/client.c b/net/9p/client.c -index 9c9d249dabae..84b1b0fb8e8a 100644 ---- a/net/9p/client.c -+++ b/net/9p/client.c -@@ -132,6 +132,59 @@ static int get_protocol_version(char *s) - * Return 0 upon success, -ERRNO upon failure - */ - -+/* -+ * Shared-client registry. -+ * -+ * Some transports (currently only Xen 9pfs) expose a single endpoint that -+ * cannot multiplex more than one p9_client: a second mount of the same -+ * endpoint would clobber the first's client pointer. For such transports -+ * (p9_trans_module.share_client) all mounts of one endpoint share a single -+ * refcounted p9_client and tell themselves apart by attaching (Tattach) with -+ * their own aname, which gives each an independent tree and superblock. -+ * -+ * Clients are keyed by endpoint id: the explicit "tag=" mount option when the -+ * mount supplied one, otherwise the mount device name. -+ */ -+static LIST_HEAD(p9_shared_clients); -+static DEFINE_MUTEX(p9_shared_clients_lock); -+ -+/* Value of the "tag=" mount option in @options, copied into @buf, or NULL when -+ * absent. Matched only at an option boundary. -+ */ -+static const char *p9_options_tag(const char *options, char *buf, size_t buflen) -+{ -+ const char *p = options; -+ -+ while (p && *p) { -+ if (!strncmp(p, "tag=", 4)) { -+ size_t n = strcspn(p + 4, ","); -+ -+ if (n == 0 || n >= buflen) -+ return NULL; -+ memcpy(buf, p + 4, n); -+ buf[n] = '\0'; -+ return buf; -+ } -+ p = strchr(p, ','); -+ if (p) -+ p++; -+ } -+ return NULL; -+} -+ -+/* Caller must hold p9_shared_clients_lock. */ -+static struct p9_client *p9_client_find_shared(struct p9_trans_module *trans, -+ const char *key) -+{ -+ struct p9_client *clnt; -+ -+ list_for_each_entry(clnt, &p9_shared_clients, shared_list) { -+ if (clnt->trans_mod == trans && !strcmp(clnt->shared_key, key)) -+ return clnt; -+ } -+ return NULL; -+} -+ - static int parse_opts(char *opts, struct p9_client *clnt) - { - char *options, *tmp_options; -@@ -981,6 +1034,7 @@ struct p9_client *p9_client_create(const char *dev_name, char *options) - struct p9_client *clnt; - char *client_id; - char *cache_name; -+ bool shared_locked = false; - - clnt = kmalloc(sizeof(*clnt), GFP_KERNEL); - if (!clnt) -@@ -996,6 +1050,9 @@ struct p9_client *p9_client_create(const char *dev_name, char *options) - spin_lock_init(&clnt->lock); - idr_init(&clnt->fids); - idr_init(&clnt->reqs); -+ refcount_set(&clnt->refcount, 1); -+ INIT_LIST_HEAD(&clnt->shared_list); -+ clnt->shared_key = NULL; - - err = parse_opts(options, clnt); - if (err < 0) -@@ -1014,6 +1071,47 @@ struct p9_client *p9_client_create(const char *dev_name, char *options) - p9_debug(P9_DEBUG_MUX, "clnt %p trans %p msize %d protocol %d\n", - clnt, clnt->trans_mod, clnt->msize, clnt->proto_version); - -+ if (clnt->trans_mod->share_client) { -+ char tagbuf[64]; -+ const char *key = p9_options_tag(options, tagbuf, sizeof(tagbuf)); -+ struct p9_client *shared; -+ -+ if (!key) -+ key = dev_name; -+ if (!key) { -+ err = -EINVAL; -+ goto put_trans; -+ } -+ mutex_lock(&p9_shared_clients_lock); -+ shared = p9_client_find_shared(clnt->trans_mod, key); -+ if (shared) { -+ /* Endpoint already has a client; reuse it. Discard the -+ * client we speculatively allocated above. -+ */ -+ refcount_inc(&shared->refcount); -+ mutex_unlock(&p9_shared_clients_lock); -+ v9fs_put_trans(clnt->trans_mod); -+ idr_destroy(&clnt->reqs); -+ idr_destroy(&clnt->fids); -+ kfree(clnt); -+ return shared; -+ } -+ /* First mount of this endpoint. Hold the registry lock across -+ * setup (trans->create + version negotiation) so a concurrent -+ * mount of the same endpoint waits and then finds a fully -+ * initialised client, rather than racing trans->create which -+ * binds the endpoint to a single client. share_client -+ * transports have a local backend, so the stall is bounded. -+ */ -+ clnt->shared_key = kstrdup(key, GFP_KERNEL); -+ if (!clnt->shared_key) { -+ mutex_unlock(&p9_shared_clients_lock); -+ err = -ENOMEM; -+ goto put_trans; -+ } -+ shared_locked = true; -+ } -+ - err = clnt->trans_mod->create(clnt, dev_name, options); - if (err) - goto put_trans; -@@ -1054,6 +1152,10 @@ struct p9_client *p9_client_create(const char *dev_name, char *options) - NULL); - - kfree(cache_name); -+ if (shared_locked) { -+ list_add(&clnt->shared_list, &p9_shared_clients); -+ mutex_unlock(&p9_shared_clients_lock); -+ } - return clnt; - - close_trans: -@@ -1061,6 +1163,10 @@ struct p9_client *p9_client_create(const char *dev_name, char *options) - put_trans: - v9fs_put_trans(clnt->trans_mod); - free_client: -+ if (shared_locked) { -+ kfree(clnt->shared_key); -+ mutex_unlock(&p9_shared_clients_lock); -+ } - kfree(clnt); - return ERR_PTR(err); - } -@@ -1073,6 +1179,18 @@ void p9_client_destroy(struct p9_client *clnt) - - p9_debug(P9_DEBUG_MUX, "clnt %p\n", clnt); - -+ /* Shared client: only the last mount tears it down. */ -+ if (clnt->shared_key) { -+ mutex_lock(&p9_shared_clients_lock); -+ if (!refcount_dec_and_test(&clnt->refcount)) { -+ mutex_unlock(&p9_shared_clients_lock); -+ return; -+ } -+ list_del(&clnt->shared_list); -+ mutex_unlock(&p9_shared_clients_lock); -+ kfree(clnt->shared_key); -+ } -+ - if (clnt->trans_mod) - clnt->trans_mod->close(clnt); - -@@ -1093,6 +1211,11 @@ EXPORT_SYMBOL(p9_client_destroy); - void p9_client_disconnect(struct p9_client *clnt) - { - p9_debug(P9_DEBUG_9P, "clnt %p\n", clnt); -+ /* On a shared client, only the last mount may tear the link down; -+ * disconnecting while a sibling mount is still live would break it. -+ */ -+ if (clnt->shared_key && refcount_read(&clnt->refcount) > 1) -+ return; - clnt->status = Disconnected; - } - EXPORT_SYMBOL(p9_client_disconnect); -@@ -1100,6 +1223,8 @@ EXPORT_SYMBOL(p9_client_disconnect); - void p9_client_begin_disconnect(struct p9_client *clnt) - { - p9_debug(P9_DEBUG_9P, "clnt %p\n", clnt); -+ if (clnt->shared_key && refcount_read(&clnt->refcount) > 1) -+ return; - clnt->status = BeginDisconnect; - } - EXPORT_SYMBOL(p9_client_begin_disconnect); -diff --git a/net/9p/trans_xen.c b/net/9p/trans_xen.c -index 068d57515dd5..9960a7e8246e 100644 ---- a/net/9p/trans_xen.c -+++ b/net/9p/trans_xen.c -@@ -66,16 +66,49 @@ static int p9_xen_cancel(struct p9_client *client, struct p9_req_t *req) - return 1; - } - -+/* Value of the "tag=" mount option in @args, copied into @buf, or NULL when -+ * absent. Matched only at an option boundary. The device is selected by tag, -+ * so several mounts of one frontend can carry distinct source strings. -+ */ -+static const char *p9_xen_opt_tag(const char *args, char *buf, size_t buflen) -+{ -+ const char *p = args; -+ -+ while (p && *p) { -+ if (!strncmp(p, "tag=", 4)) { -+ size_t n = strcspn(p + 4, ","); -+ -+ if (n == 0 || n >= buflen) -+ return NULL; -+ memcpy(buf, p + 4, n); -+ buf[n] = '\0'; -+ return buf; -+ } -+ p = strchr(p, ','); -+ if (p) -+ p++; -+ } -+ return NULL; -+} -+ - static int p9_xen_create(struct p9_client *client, const char *addr, char *args) - { - struct xen_9pfs_front_priv *priv; -- -- if (addr == NULL) -+ char tagbuf[64]; -+ const char *tag; -+ -+ /* Prefer an explicit tag= option so the mount source can differ per -+ * mount (distinct /proc/mounts device names); fall back to the source. -+ */ -+ tag = p9_xen_opt_tag(args, tagbuf, sizeof(tagbuf)); -+ if (!tag) -+ tag = addr; -+ if (tag == NULL) - return -EINVAL; - - read_lock(&xen_9pfs_lock); - list_for_each_entry(priv, &xen_9pfs_devs, list) { -- if (!strcmp(priv->tag, addr)) { -+ if (!strcmp(priv->tag, tag)) { - priv->client = client; - read_unlock(&xen_9pfs_lock); - return 0; -@@ -258,6 +291,7 @@ static struct p9_trans_module p9_xen_trans = { - .maxsize = 1 << (XEN_9PFS_RING_ORDER + XEN_PAGE_SHIFT - 2), - .pooled_rbuffers = false, - .def = 1, -+ .share_client = true, - .create = p9_xen_create, - .close = p9_xen_close, - .request = p9_xen_request, --- -2.55.0 - diff --git a/patches/9pfs-xen-multi-attach.patch b/patches/9pfs-xen-multi-attach.patch deleted file mode 100644 index 0b6ea59e..00000000 --- a/patches/9pfs-xen-multi-attach.patch +++ /dev/null @@ -1,372 +0,0 @@ -From 4e7625e42cff86b597e335dd8a58c8ce25b70c98 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Tue, 14 Jul 2026 22:43:35 -0700 -Subject: [PATCH] 9p/xen: share one frontend across mounts and advertise it - -The Xen 9pfs transport binds a single p9_client to each frontend -endpoint, so a second mount of the same tag clobbers the first. Let the -9p core share one refcounted p9_client across all mounts of an endpoint -(opt-in via p9_trans_module.share_client, set for xen), so several mounts -share a frontend, each attaching with its own aname to get an independent -tree and superblock. - -Add a tag= mount option so the endpoint id is decoupled from the mount -source string (distinct /proc/mounts device names per mount), and -advertise the capability to userspace as edera_multi_attach_v1 in -/sys/fs/9p/features so it can probe support before relying on it. - -Signed-off-by: Alex Zenla ---- - fs/9p/v9fs.c | 32 ++++++++++- - fs/9p/vfs_super.c | 1 + - include/net/9p/client.h | 16 ++++++ - include/net/9p/transport.h | 9 ++++ - net/9p/client.c | 107 +++++++++++++++++++++++++++++++++++++ - net/9p/trans_xen.c | 9 +++- - 6 files changed, 172 insertions(+), 2 deletions(-) - -diff --git a/fs/9p/v9fs.c b/fs/9p/v9fs.c -index 057487efaaeb..e99993ec229e 100644 ---- a/fs/9p/v9fs.c -+++ b/fs/9p/v9fs.c -@@ -50,7 +50,7 @@ enum { - Opt_locktimeout, - - /* Client options */ -- Opt_msize, Opt_trans, Opt_legacy, Opt_version, -+ Opt_msize, Opt_trans, Opt_legacy, Opt_version, Opt_tag, - - /* fd transport options */ - /* Options that take integer arguments */ -@@ -99,6 +99,7 @@ const struct fs_parameter_spec v9fs_param_spec[] = { - fsparam_flag ("noextend", Opt_legacy), - fsparam_string ("trans", Opt_trans), - fsparam_enum ("version", Opt_version, p9_versions), -+ fsparam_string ("tag", Opt_tag), - - /* fd transport options */ - fsparam_u32 ("rfdno", Opt_rfdno), -@@ -267,6 +268,15 @@ int v9fs_parse_param(struct fs_context *fc, struct fs_parameter *param) - session_opts->aname = param->string; - param->string = NULL; - break; -+ case Opt_tag: -+ /* Explicit transport endpoint id (Xen 9pfs tag), decoupled from -+ * the mount source so one endpoint can back several mounts that -+ * each show a distinct source in /proc/mounts. -+ */ -+ kfree(ctx->tag); -+ ctx->tag = param->string; -+ param->string = NULL; -+ break; - case Opt_nodevmap: - session_opts->nodev = 1; - break; -@@ -612,10 +622,30 @@ static ssize_t caches_show(struct kobject *kobj, - static struct kobj_attribute v9fs_attr_cache = __ATTR_RO(caches); - #endif /* CONFIG_9P_FSCACHE */ - -+/* -+ * Capability tokens for userspace to probe, one per line. -+ * -+ * "edera_multi_attach_v1": a transport that sets p9_trans_module.share_client -+ * (e.g. Xen 9pfs) can back several mounts of a single endpoint, each attaching -+ * with its own aname. Userspace can test for this token before mounting subtrees -+ * of one frontend as independent superblocks (distinct st_dev) instead of -+ * bind-mounting from a single mount. The token is vendor-namespaced and -+ * versioned so it never aliases an unrelated upstream feature name, and a future -+ * behavior change can advertise "_v2" instead. -+ */ -+static ssize_t features_show(struct kobject *kobj, -+ struct kobj_attribute *attr, char *buf) -+{ -+ return sysfs_emit(buf, "edera_multi_attach_v1\n"); -+} -+ -+static struct kobj_attribute v9fs_attr_features = __ATTR_RO(features); -+ - static struct attribute *v9fs_attrs[] = { - #ifdef CONFIG_9P_FSCACHE - &v9fs_attr_cache.attr, - #endif -+ &v9fs_attr_features.attr, - NULL, - }; - -diff --git a/fs/9p/vfs_super.c b/fs/9p/vfs_super.c -index 315336de6f02..ba67eb19147a 100644 ---- a/fs/9p/vfs_super.c -+++ b/fs/9p/vfs_super.c -@@ -293,6 +293,7 @@ static void v9fs_free_fc(struct fs_context *fc) - #ifdef CONFIG_9P_FSCACHE - kfree(ctx->session_opts.cachetag); - #endif -+ kfree(ctx->tag); - if (ctx->client_opts.trans_mod) - v9fs_put_trans(ctx->client_opts.trans_mod); - kfree(ctx); -diff --git a/include/net/9p/client.h b/include/net/9p/client.h -index 838a94218b59..76d44f85de11 100644 ---- a/include/net/9p/client.h -+++ b/include/net/9p/client.h -@@ -129,6 +129,16 @@ struct p9_client { - struct idr fids; - struct idr reqs; - -+ /* Client sharing, for transports with p9_trans_module.share_client set: -+ * all mounts of one endpoint reference a single client. @refcount counts -+ * those mounts; @shared_list links the client into the shared-client -+ * registry, keyed by @shared_key (a copy of the endpoint tag/source). -+ * @shared_key is NULL for ordinary, unshared clients. -+ */ -+ refcount_t refcount; -+ struct list_head shared_list; -+ char *shared_key; -+ - char name[__NEW_UTS_LEN + 1]; - }; - -@@ -220,6 +230,12 @@ struct v9fs_context { - struct p9_fd_opts fd_opts; - struct p9_rdma_opts rdma_opts; - struct p9_session_opts session_opts; -+ /* Optional explicit transport endpoint id (the Xen 9pfs "tag"). When -+ * set it selects the device and keys client sharing, decoupling the -+ * endpoint from the mount source string so each mount of one endpoint -+ * can present a distinct source in /proc/mounts. -+ */ -+ char *tag; - }; - - /** -diff --git a/include/net/9p/transport.h b/include/net/9p/transport.h -index a912bbaa862f..3a390962c563 100644 ---- a/include/net/9p/transport.h -+++ b/include/net/9p/transport.h -@@ -34,6 +34,14 @@ - * @supports_vmalloc: set if this transport can work with vmalloc'd buffers - * (non-physically contiguous memory). Transports requiring - * DMA should leave this as false. -+ * @share_client: set if a single transport endpoint (e.g. one Xen 9pfs -+ * frontend/backend pair, identified by its tag) can back more -+ * than one mount. Such an endpoint cannot multiplex several -+ * p9_clients, so when this is set the 9p core hands every mount -+ * of the same endpoint a single, refcounted p9_client instead of -+ * creating one per mount. Each mount still issues its own Tattach -+ * (with its own aname), so it gets an independent tree and -+ * superblock over the shared client. - * @create: member function to create a new connection on this transport - * @close: member function to discard a connection on this transport - * @request: member function to issue a request to the transport -@@ -55,6 +63,7 @@ struct p9_trans_module { - bool pooled_rbuffers; - bool def; /* this transport should be default */ - bool supports_vmalloc; /* can work with vmalloc'd buffers */ -+ bool share_client; /* one endpoint may back many mounts */ - struct module *owner; - int (*create)(struct p9_client *client, - struct fs_context *fc); -diff --git a/net/9p/client.c b/net/9p/client.c -index f60d1d041adb..4273ef97c12b 100644 ---- a/net/9p/client.c -+++ b/net/9p/client.c -@@ -81,6 +81,44 @@ static int safe_errno(int err) - return err; - } - -+/* -+ * Shared-client registry. -+ * -+ * Some transports (currently only Xen 9pfs) expose a single endpoint that -+ * cannot multiplex more than one p9_client: a second mount of the same -+ * endpoint would clobber the first's client pointer. For such transports -+ * (p9_trans_module.share_client) all mounts of one endpoint share a single -+ * refcounted p9_client and tell themselves apart by attaching (Tattach) with -+ * their own aname, which gives each an independent tree and superblock. -+ * -+ * Clients are keyed by endpoint id: the explicit transport tag when the mount -+ * supplied one (tag=), otherwise the mount source string. -+ */ -+static LIST_HEAD(p9_shared_clients); -+static DEFINE_MUTEX(p9_shared_clients_lock); -+ -+static const char *p9_client_share_key(struct fs_context *fc) -+{ -+ struct v9fs_context *ctx = fc->fs_private; -+ -+ if (ctx && ctx->tag) -+ return ctx->tag; -+ return fc->source; -+} -+ -+/* Caller must hold p9_shared_clients_lock. */ -+static struct p9_client *p9_client_find_shared(struct p9_trans_module *trans, -+ const char *key) -+{ -+ struct p9_client *clnt; -+ -+ list_for_each_entry(clnt, &p9_shared_clients, shared_list) { -+ if (clnt->trans_mod == trans && !strcmp(clnt->shared_key, key)) -+ return clnt; -+ } -+ return NULL; -+} -+ - static int apply_client_options(struct p9_client *clnt, struct fs_context *fc) - { - struct v9fs_context *ctx = fc->fs_private; -@@ -858,6 +896,7 @@ struct p9_client *p9_client_create(struct fs_context *fc) - struct p9_client *clnt; - char *client_id; - char *cache_name; -+ bool shared_locked = false; - - clnt = kmalloc(sizeof(*clnt), GFP_KERNEL); - if (!clnt) -@@ -873,6 +912,9 @@ struct p9_client *p9_client_create(struct fs_context *fc) - spin_lock_init(&clnt->lock); - idr_init(&clnt->fids); - idr_init(&clnt->reqs); -+ refcount_set(&clnt->refcount, 1); -+ INIT_LIST_HEAD(&clnt->shared_list); -+ clnt->shared_key = NULL; - - err = apply_client_options(clnt, fc); - if (err) -@@ -891,6 +933,44 @@ struct p9_client *p9_client_create(struct fs_context *fc) - p9_debug(P9_DEBUG_MUX, "clnt %p trans %p msize %d protocol %d\n", - clnt, clnt->trans_mod, clnt->msize, clnt->proto_version); - -+ if (clnt->trans_mod->share_client) { -+ const char *key = p9_client_share_key(fc); -+ struct p9_client *shared; -+ -+ if (!key) { -+ err = -EINVAL; -+ goto put_trans; -+ } -+ mutex_lock(&p9_shared_clients_lock); -+ shared = p9_client_find_shared(clnt->trans_mod, key); -+ if (shared) { -+ /* Endpoint already has a client; reuse it. Discard the -+ * client we speculatively allocated above. -+ */ -+ refcount_inc(&shared->refcount); -+ mutex_unlock(&p9_shared_clients_lock); -+ v9fs_put_trans(clnt->trans_mod); -+ idr_destroy(&clnt->reqs); -+ idr_destroy(&clnt->fids); -+ kfree(clnt); -+ return shared; -+ } -+ /* First mount of this endpoint. Hold the registry lock across -+ * setup (trans->create + version negotiation) so a concurrent -+ * mount of the same endpoint waits and then finds a fully -+ * initialised client, rather than racing trans->create which -+ * binds the endpoint to a single client. share_client -+ * transports have a local backend, so the stall is bounded. -+ */ -+ clnt->shared_key = kstrdup(key, GFP_KERNEL); -+ if (!clnt->shared_key) { -+ mutex_unlock(&p9_shared_clients_lock); -+ err = -ENOMEM; -+ goto put_trans; -+ } -+ shared_locked = true; -+ } -+ - err = clnt->trans_mod->create(clnt, fc); - if (err) - goto put_trans; -@@ -931,6 +1011,10 @@ struct p9_client *p9_client_create(struct fs_context *fc) - NULL); - - kfree(cache_name); -+ if (shared_locked) { -+ list_add(&clnt->shared_list, &p9_shared_clients); -+ mutex_unlock(&p9_shared_clients_lock); -+ } - return clnt; - - close_trans: -@@ -938,6 +1022,10 @@ struct p9_client *p9_client_create(struct fs_context *fc) - put_trans: - v9fs_put_trans(clnt->trans_mod); - free_client: -+ if (shared_locked) { -+ kfree(clnt->shared_key); -+ mutex_unlock(&p9_shared_clients_lock); -+ } - kfree(clnt); - return ERR_PTR(err); - } -@@ -950,6 +1038,18 @@ void p9_client_destroy(struct p9_client *clnt) - - p9_debug(P9_DEBUG_MUX, "clnt %p\n", clnt); - -+ /* Shared client: only the last mount tears it down. */ -+ if (clnt->shared_key) { -+ mutex_lock(&p9_shared_clients_lock); -+ if (!refcount_dec_and_test(&clnt->refcount)) { -+ mutex_unlock(&p9_shared_clients_lock); -+ return; -+ } -+ list_del(&clnt->shared_list); -+ mutex_unlock(&p9_shared_clients_lock); -+ kfree(clnt->shared_key); -+ } -+ - if (clnt->trans_mod) - clnt->trans_mod->close(clnt); - -@@ -970,6 +1070,11 @@ EXPORT_SYMBOL(p9_client_destroy); - void p9_client_disconnect(struct p9_client *clnt) - { - p9_debug(P9_DEBUG_9P, "clnt %p\n", clnt); -+ /* On a shared client, only the last mount may tear the link down; -+ * disconnecting while a sibling mount is still live would break it. -+ */ -+ if (clnt->shared_key && refcount_read(&clnt->refcount) > 1) -+ return; - clnt->status = Disconnected; - } - EXPORT_SYMBOL(p9_client_disconnect); -@@ -977,6 +1082,8 @@ EXPORT_SYMBOL(p9_client_disconnect); - void p9_client_begin_disconnect(struct p9_client *clnt) - { - p9_debug(P9_DEBUG_9P, "clnt %p\n", clnt); -+ if (clnt->shared_key && refcount_read(&clnt->refcount) > 1) -+ return; - clnt->status = BeginDisconnect; - } - EXPORT_SYMBOL(p9_client_begin_disconnect); -diff --git a/net/9p/trans_xen.c b/net/9p/trans_xen.c -index 12f752a92332..edc58c16432d 100644 ---- a/net/9p/trans_xen.c -+++ b/net/9p/trans_xen.c -@@ -69,7 +69,13 @@ static int p9_xen_cancel(struct p9_client *client, struct p9_req_t *req) - - static int p9_xen_create(struct p9_client *client, struct fs_context *fc) - { -- const char *addr = fc->source; -+ struct v9fs_context *ctx = fc->fs_private; -+ /* Select the device by its tag. An explicit tag= lets several mounts -+ * of one frontend carry distinct source strings (distinct -+ * /proc/mounts device names); without it the source string is the tag, -+ * as before. -+ */ -+ const char *addr = (ctx && ctx->tag) ? ctx->tag : fc->source; - struct xen_9pfs_front_priv *priv; - - if (addr == NULL) -@@ -261,6 +267,7 @@ static struct p9_trans_module p9_xen_trans = { - .pooled_rbuffers = false, - .def = true, - .supports_vmalloc = false, -+ .share_client = true, - .create = p9_xen_create, - .close = p9_xen_close, - .request = p9_xen_request, --- -2.55.0 - diff --git a/patches/hack-around-pci-msix-restore-bugs-in-pv-domu.patch b/patches/hack-around-pci-msix-restore-bugs-in-pv-domu.patch deleted file mode 100644 index 62b48cbf..00000000 --- a/patches/hack-around-pci-msix-restore-bugs-in-pv-domu.patch +++ /dev/null @@ -1,29 +0,0 @@ -From c634842013a1722f3e157876682aa39be14831f3 Mon Sep 17 00:00:00 2001 -From: Benjamin Leggett -Date: Wed, 4 Jun 2025 12:20:45 -0400 -Subject: [PATCH] edera: Hack around PCI MSIX restore bugs in PV domU - ---- - arch/x86/pci/xen.c | 6 ++++++ - 1 file changed, 6 insertions(+) - -diff --git a/arch/x86/pci/xen.c b/arch/x86/pci/xen.c -index 0f2fe524f60d..7be968e90f67 100644 ---- a/arch/x86/pci/xen.c -+++ b/arch/x86/pci/xen.c -@@ -354,6 +354,12 @@ static int xen_initdom_setup_msi_irqs(struct pci_dev *dev, int nvec, int type) - bool xen_initdom_restore_msi(struct pci_dev *dev) - { - int ret = 0; -+ // TODO(bmleggett) this effectively -+ // causes MSI writes to be *entirely* skipped on standard PCI -+ // restore for PV domU. This works, but is (probably) not correct - -+ // something like what `pci-hyperv.c` does is (likely) we actually need. -+ if (xen_pv_domain()) -+ return false; - - if (!xen_initial_domain()) - return true; --- -2.49.0 - diff --git a/patches/hvc-xen-idm-support-1.patch b/patches/hvc-xen-idm-support-1.patch deleted file mode 100644 index 8566ee25..00000000 --- a/patches/hvc-xen-idm-support-1.patch +++ /dev/null @@ -1,111 +0,0 @@ -commit cdf0b5f73dcb55533b22b15ddaa12f0117124056 -Author: Alex Zenla -Date: Sun Nov 10 04:22:34 2024 -0500 - - hvc_xen: fix probing of second console, fixes idm support in Edera Protect - -diff --git a/drivers/tty/hvc/hvc_xen.c b/drivers/tty/hvc/hvc_xen.c -index 7948660e042f..6724b8d97d2b 100644 ---- a/drivers/tty/hvc/hvc_xen.c -+++ b/drivers/tty/hvc/hvc_xen.c -@@ -37,8 +37,6 @@ struct xencons_info { - struct xenbus_device *xbdev; - struct xencons_interface *intf; - unsigned int evtchn; -- XENCONS_RING_IDX out_cons; -- unsigned int out_cons_same; - struct hvc_struct *hvc; - int irq; - int vtermno; -@@ -88,11 +86,7 @@ static int __write_console(struct xencons_info *xencons, - cons = intf->out_cons; - prod = intf->out_prod; - mb(); /* update queue values before going on */ -- -- if ((prod - cons) > sizeof(intf->out)) { -- pr_err_once("xencons: Illegal ring page indices"); -- return -EINVAL; -- } -+ BUG_ON((prod - cons) > sizeof(intf->out)); - - while ((sent < len) && ((prod - cons) < sizeof(intf->out))) - intf->out[MASK_XENCONS_IDX(prod++, intf->out)] = data[sent++]; -@@ -121,9 +115,6 @@ static int domU_write_console(uint32_t vtermno, const char *data, int len) - while (len) { - int sent = __write_console(cons, data, len); - -- if (sent < 0) -- return sent; -- - data += sent; - len -= sent; - -@@ -140,8 +131,6 @@ static int domU_read_console(uint32_t vtermno, char *buf, int len) - XENCONS_RING_IDX cons, prod; - int recv = 0; - struct xencons_info *xencons = vtermno_to_xencons(vtermno); -- unsigned int eoiflag = 0; -- - if (xencons == NULL) - return -EINVAL; - intf = xencons->intf; -@@ -149,11 +138,7 @@ static int domU_read_console(uint32_t vtermno, char *buf, int len) - cons = intf->in_cons; - prod = intf->in_prod; - mb(); /* get pointers before reading ring */ -- -- if ((prod - cons) > sizeof(intf->in)) { -- pr_err_once("xencons: Illegal ring page indices"); -- return -EINVAL; -- } -+ BUG_ON((prod - cons) > sizeof(intf->in)); - - while (cons != prod && recv < len) - buf[recv++] = intf->in[MASK_XENCONS_IDX(cons++, intf->in)]; -@@ -161,27 +146,7 @@ static int domU_read_console(uint32_t vtermno, char *buf, int len) - mb(); /* read ring before consuming */ - intf->in_cons = cons; - -- /* -- * When to mark interrupt having been spurious: -- * - there was no new data to be read, and -- * - the backend did not consume some output bytes, and -- * - the previous round with no read data didn't see consumed bytes -- * (we might have a race with an interrupt being in flight while -- * updating xencons->out_cons, so account for that by allowing one -- * round without any visible reason) -- */ -- if (intf->out_cons != xencons->out_cons) { -- xencons->out_cons = intf->out_cons; -- xencons->out_cons_same = 0; -- } -- if (recv) { -- notify_daemon(xencons); -- } else if (xencons->out_cons_same++ > 1) { -- eoiflag = XEN_EOI_FLAG_SPURIOUS; -- } -- -- xen_irq_lateeoi(xencons->irq, eoiflag); -- -+ notify_daemon(xencons); - return recv; - } - -@@ -410,7 +375,7 @@ static int xencons_connect_backend(struct xenbus_device *dev, - if (ret) - return ret; - info->evtchn = evtchn; -- irq = bind_interdomain_evtchn_to_irq_lateeoi(dev->otherend_id, evtchn); -+ irq = bind_evtchn_to_irq(evtchn); - if (irq < 0) - return irq; - info->irq = irq; -@@ -574,7 +539,7 @@ static int __init xen_hvc_init(void) - return r; - - info = vtermno_to_xencons(HVC_COOKIE); -- info->irq = bind_evtchn_to_irq_lateeoi(info->evtchn); -+ info->irq = bind_evtchn_to_irq(info->evtchn); - } - if (info->irq < 0) - info->irq = 0; /* NO_IRQ */ diff --git a/patches/hvc-xen-idm-support-2.patch b/patches/hvc-xen-idm-support-2.patch deleted file mode 100644 index 83a9fa6a..00000000 --- a/patches/hvc-xen-idm-support-2.patch +++ /dev/null @@ -1,381 +0,0 @@ -commit 2f49a6cbd11cf5d007e5492d1ad09cdd02a3c315 -Author: Alex Zenla -Date: Mon Nov 11 01:05:45 2024 -0500 - - hvc_xen: fix probing of second console, fixes idm support in Edera Protect - -diff --git a/drivers/tty/hvc/hvc_xen.c b/drivers/tty/hvc/hvc_xen.c -index 4886cad0fde6..92c9a476defc 100644 ---- a/drivers/tty/hvc/hvc_xen.c -+++ b/drivers/tty/hvc/hvc_xen.c -@@ -37,13 +37,10 @@ struct xencons_info { - struct xenbus_device *xbdev; - struct xencons_interface *intf; - unsigned int evtchn; -- XENCONS_RING_IDX out_cons; -- unsigned int out_cons_same; - struct hvc_struct *hvc; - int irq; - int vtermno; - grant_ref_t gntref; -- spinlock_t ring_lock; - }; - - static LIST_HEAD(xenconsoles); -@@ -53,22 +50,17 @@ static DEFINE_SPINLOCK(xencons_lock); - - static struct xencons_info *vtermno_to_xencons(int vtermno) - { -- struct xencons_info *entry, *ret = NULL; -- unsigned long flags; -+ struct xencons_info *entry, *n, *ret = NULL; - -- spin_lock_irqsave(&xencons_lock, flags); -- if (list_empty(&xenconsoles)) { -- spin_unlock_irqrestore(&xencons_lock, flags); -- return NULL; -- } -+ if (list_empty(&xenconsoles)) -+ return NULL; - -- list_for_each_entry(entry, &xenconsoles, list) { -+ list_for_each_entry_safe(entry, n, &xenconsoles, list) { - if (entry->vtermno == vtermno) { - ret = entry; - break; - } - } -- spin_unlock_irqrestore(&xencons_lock, flags); - - return ret; - } -@@ -90,25 +82,17 @@ static int __write_console(struct xencons_info *xencons, - XENCONS_RING_IDX cons, prod; - struct xencons_interface *intf = xencons->intf; - int sent = 0; -- unsigned long flags; - -- spin_lock_irqsave(&xencons->ring_lock, flags); - cons = intf->out_cons; - prod = intf->out_prod; - mb(); /* update queue values before going on */ -- -- if ((prod - cons) > sizeof(intf->out)) { -- spin_unlock_irqrestore(&xencons->ring_lock, flags); -- pr_err_once("xencons: Illegal ring page indices"); -- return -EINVAL; -- } -+ BUG_ON((prod - cons) > sizeof(intf->out)); - - while ((sent < len) && ((prod - cons) < sizeof(intf->out))) - intf->out[MASK_XENCONS_IDX(prod++, intf->out)] = data[sent++]; - - wmb(); /* write ring before updating pointer */ - intf->out_prod = prod; -- spin_unlock_irqrestore(&xencons->ring_lock, flags); - - if (sent) - notify_daemon(xencons); -@@ -130,10 +114,7 @@ static int domU_write_console(uint32_t vtermno, const char *data, int len) - */ - while (len) { - int sent = __write_console(cons, data, len); -- -- if (sent < 0) -- return sent; -- -+ - data += sent; - len -= sent; - -@@ -150,23 +131,14 @@ static int domU_read_console(uint32_t vtermno, char *buf, int len) - XENCONS_RING_IDX cons, prod; - int recv = 0; - struct xencons_info *xencons = vtermno_to_xencons(vtermno); -- unsigned int eoiflag = 0; -- unsigned long flags; -- - if (xencons == NULL) - return -EINVAL; - intf = xencons->intf; - -- spin_lock_irqsave(&xencons->ring_lock, flags); - cons = intf->in_cons; - prod = intf->in_prod; - mb(); /* get pointers before reading ring */ -- -- if ((prod - cons) > sizeof(intf->in)) { -- spin_unlock_irqrestore(&xencons->ring_lock, flags); -- pr_err_once("xencons: Illegal ring page indices"); -- return -EINVAL; -- } -+ BUG_ON((prod - cons) > sizeof(intf->in)); - - while (cons != prod && recv < len) - buf[recv++] = intf->in[MASK_XENCONS_IDX(cons++, intf->in)]; -@@ -174,30 +146,7 @@ static int domU_read_console(uint32_t vtermno, char *buf, int len) - mb(); /* read ring before consuming */ - intf->in_cons = cons; - -- /* -- * When to mark interrupt having been spurious: -- * - there was no new data to be read, and -- * - the backend did not consume some output bytes, and -- * - the previous round with no read data didn't see consumed bytes -- * (we might have a race with an interrupt being in flight while -- * updating xencons->out_cons, so account for that by allowing one -- * round without any visible reason) -- */ -- if (intf->out_cons != xencons->out_cons) { -- xencons->out_cons = intf->out_cons; -- xencons->out_cons_same = 0; -- } -- if (!recv && xencons->out_cons_same++ > 1) { -- eoiflag = XEN_EOI_FLAG_SPURIOUS; -- } -- spin_unlock_irqrestore(&xencons->ring_lock, flags); -- -- if (recv) { -- notify_daemon(xencons); -- } -- -- xen_irq_lateeoi(xencons->irq, eoiflag); -- -+ notify_daemon(xencons); - return recv; - } - -@@ -239,7 +188,7 @@ static int xen_hvm_console_init(void) - { - int r; - uint64_t v = 0; -- unsigned long gfn, flags; -+ unsigned long gfn; - struct xencons_info *info; - - if (!xen_hvm_domain()) -@@ -250,7 +199,6 @@ static int xen_hvm_console_init(void) - info = kzalloc(sizeof(struct xencons_info), GFP_KERNEL); - if (!info) - return -ENOMEM; -- spin_lock_init(&info->ring_lock); - } else if (info->intf != NULL) { - /* already configured */ - return 0; -@@ -275,9 +223,9 @@ static int xen_hvm_console_init(void) - goto err; - info->vtermno = HVC_COOKIE; - -- spin_lock_irqsave(&xencons_lock, flags); -+ spin_lock(&xencons_lock); - list_add_tail(&info->list, &xenconsoles); -- spin_unlock_irqrestore(&xencons_lock, flags); -+ spin_unlock(&xencons_lock); - - return 0; - err: -@@ -287,7 +235,6 @@ static int xen_hvm_console_init(void) - - static int xencons_info_pv_init(struct xencons_info *info, int vtermno) - { -- spin_lock_init(&info->ring_lock); - info->evtchn = xen_start_info->console.domU.evtchn; - /* GFN == MFN for PV guest */ - info->intf = gfn_to_virt(xen_start_info->console.domU.mfn); -@@ -301,7 +248,6 @@ static int xencons_info_pv_init(struct xencons_info *info, int vtermno) - static int xen_pv_console_init(void) - { - struct xencons_info *info; -- unsigned long flags; - - if (!xen_pv_domain()) - return -ENODEV; -@@ -318,9 +264,9 @@ static int xen_pv_console_init(void) - /* already configured */ - return 0; - } -- spin_lock_irqsave(&xencons_lock, flags); -+ spin_lock(&xencons_lock); - xencons_info_pv_init(info, HVC_COOKIE); -- spin_unlock_irqrestore(&xencons_lock, flags); -+ spin_unlock(&xencons_lock); - - return 0; - } -@@ -328,7 +274,6 @@ static int xen_pv_console_init(void) - static int xen_initial_domain_console_init(void) - { - struct xencons_info *info; -- unsigned long flags; - - if (!xen_initial_domain()) - return -ENODEV; -@@ -338,15 +283,14 @@ static int xen_initial_domain_console_init(void) - info = kzalloc(sizeof(struct xencons_info), GFP_KERNEL); - if (!info) - return -ENOMEM; -- spin_lock_init(&info->ring_lock); - } - - info->irq = bind_virq_to_irq(VIRQ_CONSOLE, 0, false); - info->vtermno = HVC_COOKIE; - -- spin_lock_irqsave(&xencons_lock, flags); -+ spin_lock(&xencons_lock); - list_add_tail(&info->list, &xenconsoles); -- spin_unlock_irqrestore(&xencons_lock, flags); -+ spin_unlock(&xencons_lock); - - return 0; - } -@@ -377,21 +321,18 @@ void xen_console_resume(void) - #ifdef CONFIG_HVC_XEN_FRONTEND - static void xencons_disconnect_backend(struct xencons_info *info) - { -- if (info->hvc != NULL) -- hvc_remove(info->hvc); -- info->hvc = NULL; -- if (info->irq > 0) { -- evtchn_put(info->evtchn); -- info->irq = 0; -- info->evtchn = 0; -- } -- /* evtchn_put() will also close it so this is only an error path */ -+ if (info->irq > 0) -+ unbind_from_irqhandler(info->irq, NULL); -+ info->irq = 0; - if (info->evtchn > 0) - xenbus_free_evtchn(info->xbdev, info->evtchn); - info->evtchn = 0; - if (info->gntref > 0) - gnttab_free_grant_references(info->gntref); - info->gntref = 0; -+ if (info->hvc != NULL) -+ hvc_remove(info->hvc); -+ info->hvc = NULL; - } - - static void xencons_free(struct xencons_info *info) -@@ -404,12 +345,10 @@ static void xencons_free(struct xencons_info *info) - - static int xen_console_remove(struct xencons_info *info) - { -- unsigned long flags; -- - xencons_disconnect_backend(info); -- spin_lock_irqsave(&xencons_lock, flags); -+ spin_lock(&xencons_lock); - list_del(&info->list); -- spin_unlock_irqrestore(&xencons_lock, flags); -+ spin_unlock(&xencons_lock); - if (info->xbdev != NULL) - xencons_free(info); - else { -@@ -436,7 +375,7 @@ static int xencons_connect_backend(struct xenbus_device *dev, - if (ret) - return ret; - info->evtchn = evtchn; -- irq = bind_interdomain_evtchn_to_irq_lateeoi(dev->otherend_id, evtchn); -+ irq = bind_evtchn_to_irq(evtchn); - if (irq < 0) - return irq; - info->irq = irq; -@@ -490,7 +429,6 @@ static int xencons_probe(struct xenbus_device *dev, - { - int ret, devid; - struct xencons_info *info; -- unsigned long flags; - - devid = dev->nodename[strlen(dev->nodename) - 1] - '0'; - if (devid == 0) -@@ -499,7 +437,6 @@ static int xencons_probe(struct xenbus_device *dev, - info = kzalloc(sizeof(struct xencons_info), GFP_KERNEL); - if (!info) - return -ENOMEM; -- spin_lock_init(&info->ring_lock); - dev_set_drvdata(&dev->dev, info); - info->xbdev = dev; - info->vtermno = xenbus_devid_to_vtermno(devid); -@@ -510,9 +447,9 @@ static int xencons_probe(struct xenbus_device *dev, - ret = xencons_connect_backend(dev, info); - if (ret < 0) - goto error; -- spin_lock_irqsave(&xencons_lock, flags); -+ spin_lock(&xencons_lock); - list_add_tail(&info->list, &xenconsoles); -- spin_unlock_irqrestore(&xencons_lock, flags); -+ spin_unlock(&xencons_lock); - - return 0; - -@@ -556,23 +493,10 @@ static void xencons_backend_changed(struct xenbus_device *dev, - if (dev->state == XenbusStateClosed) - break; - fallthrough; /* Missed the backend's CLOSING state */ -- case XenbusStateClosing: { -- struct xencons_info *info = dev_get_drvdata(&dev->dev);; -- -- /* -- * Don't tear down the evtchn and grant ref before the other -- * end has disconnected, but do stop userspace from trying -- * to use the device before we allow the backend to close. -- */ -- if (info->hvc) { -- hvc_remove(info->hvc); -- info->hvc = NULL; -- } -- -+ case XenbusStateClosing: - xenbus_frontend_closed(dev); - break; - } -- } - } - - static const struct xenbus_device_id xencons_ids[] = { -@@ -603,7 +527,7 @@ static int __init xen_hvc_init(void) - ops = &dom0_hvc_ops; - r = xen_initial_domain_console_init(); - if (r < 0) -- goto register_fe; -+ return r; - info = vtermno_to_xencons(HVC_COOKIE); - } else { - ops = &domU_hvc_ops; -@@ -612,10 +536,10 @@ static int __init xen_hvc_init(void) - else - r = xen_pv_console_init(); - if (r < 0) -- goto register_fe; -+ return r; - - info = vtermno_to_xencons(HVC_COOKIE); -- info->irq = bind_evtchn_to_irq_lateeoi(info->evtchn); -+ info->irq = bind_evtchn_to_irq(info->evtchn); - } - if (info->irq < 0) - info->irq = 0; /* NO_IRQ */ -@@ -624,20 +548,17 @@ static int __init xen_hvc_init(void) - - info->hvc = hvc_alloc(HVC_COOKIE, info->irq, ops, 256); - if (IS_ERR(info->hvc)) { -- unsigned long flags; -- - r = PTR_ERR(info->hvc); -- spin_lock_irqsave(&xencons_lock, flags); -+ spin_lock(&xencons_lock); - list_del(&info->list); -- spin_unlock_irqrestore(&xencons_lock, flags); -+ spin_unlock(&xencons_lock); - if (info->irq) -- evtchn_put(info->evtchn); -+ unbind_from_irqhandler(info->irq, NULL); - kfree(info); - return r; - } - - r = 0; -- register_fe: - #ifdef CONFIG_HVC_XEN_FRONTEND - r = xenbus_register_frontend(&xencons_driver); - #endif diff --git a/patches/hyperv/0001-x86-hyperv-enable-a-Xen-PV-dom0-to-use-the-L0-Hyper-.patch b/patches/hyperv/0001-x86-hyperv-enable-a-Xen-PV-dom0-to-use-the-L0-Hyper-.patch deleted file mode 100644 index ac092e1f..00000000 --- a/patches/hyperv/0001-x86-hyperv-enable-a-Xen-PV-dom0-to-use-the-L0-Hyper-.patch +++ /dev/null @@ -1,416 +0,0 @@ -From 464f276dd78fc021398e9037e920721d9699bbe6 Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Thu, 23 Jul 2026 15:42:06 -0700 -Subject: [PATCH 1/5] x86/hyperv: enable a Xen PV dom0 to use the L0 Hyper-V - host - -When Xen is itself a Hyper-V guest and hands its PV dom0 the host's -VMBus, dom0 must speak Hyper-V to the L0 host through Xen. Detect this -"nested on Xen" case (xen_pv_domain() + "Microsoft Hv" CPUID) and bring -up a minimal Hyper-V guest: GUEST_OS_ID, a hypercall page and the -per-cpu input pages, skipping the APIC/timer/isolation setup a PV dom0 -does not use. - -Add hv_nested_hostpfn()/hv_nested_hostpa() helpers translating dom0's -pseudo-physical frames to the machine (L1-physical) frames the L0 host -requires, and have hv_do_hypercall() emit machine addresses in this -mode. Reserve VIRQ_HYPERV_VMBUS and add hyperv_setup_xen_vmbus_irq() -to bind the VMBus interrupt to it. - -The helpers compile to no-ops without CONFIG_XEN_PV. - -Signed-off-by: Ariadne Conill ---- - arch/x86/hyperv/hv_init.c | 94 ++++++++++++++++++++- - arch/x86/include/asm/mshyperv.h | 30 ++++++- - arch/x86/xen/Makefile | 2 + - arch/x86/xen/hyperv.c | 144 ++++++++++++++++++++++++++++++++ - include/asm-generic/mshyperv.h | 22 +++++ - include/xen/interface/xen.h | 7 ++ - 6 files changed, 295 insertions(+), 4 deletions(-) - create mode 100644 arch/x86/xen/hyperv.c - -diff --git a/arch/x86/hyperv/hv_init.c b/arch/x86/hyperv/hv_init.c -index e890fd37e..48fbb43f2 100644 ---- a/arch/x86/hyperv/hv_init.c -+++ b/arch/x86/hyperv/hv_init.c -@@ -429,6 +429,18 @@ static void __init hv_stimer_setup_percpu_clockev(void) - old_setup_percpu_clockev(); - } - -+/* -+ * True when running as a Xen PV dom0 that is itself nested under Hyper-V (L0). -+ * Set by the Xen guest code (arch/x86/xen/) after it detects the underlying -+ * Hyper-V. In this mode Linux stays a Xen guest (x86_hyper_type == XEN_PV) but -+ * additionally brings up the VMBus stack to drive the L0 host's synthetic -+ * devices, with Xen proxying the Hyper-V enlightenments. -+ */ -+#ifdef CONFIG_XEN_PV -+bool hyperv_nested_on_xen __ro_after_init; -+EXPORT_SYMBOL_GPL(hyperv_nested_on_xen); -+#endif -+ - /* - * This function is to be invoked early in the boot sequence after the - * hypervisor has been detected. -@@ -607,6 +619,83 @@ void __init hyperv_init(void) - hv_common_free(); - } - -+/* -+ * Minimal Hyper-V bring-up for a Xen PV dom0 nested under Hyper-V. -+ * -+ * Unlike hyperv_init(), this deliberately does NOT touch the APIC, timers, TSC -+ * reference page, PCI, syscore or isolation machinery -- Xen owns all of those -+ * for a PV guest. We set up only what the VMBus stack needs: the per-cpu input -+ * pages (hv_common_init + hv_cpu_init via cpuhp), the guest OS id, and the -+ * hypercall page. Xen intercepts the GUEST_OS_ID / HYPERCALL MSR writes and -+ * fills the hypercall page with its own trap stub, and proxies the SynIC MSRs -+ * that vmbus_bus_init() subsequently programs. -+ * -+ * Must run after cpuhp is up and before hv_acpi_init() (subsys_initcall). -+ */ -+#ifdef CONFIG_XEN_PV -+void __init hyperv_init_nested_on_xen(void) -+{ -+ union hv_x64_msr_hypercall_contents hypercall_msr; -+ u64 guest_id; -+ -+ if (!hyperv_nested_on_xen) -+ return; -+ -+ /* Extract the L0 host's features and hints (subset of ms_hyperv_init). */ -+ ms_hyperv.features = cpuid_eax(HYPERV_CPUID_FEATURES); -+ ms_hyperv.priv_high = cpuid_ebx(HYPERV_CPUID_FEATURES); -+ ms_hyperv.ext_features = cpuid_ecx(HYPERV_CPUID_FEATURES); -+ ms_hyperv.misc_features = cpuid_edx(HYPERV_CPUID_FEATURES); -+ ms_hyperv.hints = cpuid_eax(HYPERV_CPUID_ENLIGHTMENT_INFO); -+ ms_hyperv.max_vp_index = cpuid_eax(HYPERV_CPUID_IMPLEMENT_LIMITS); -+ ms_hyperv.max_lp_index = cpuid_ebx(HYPERV_CPUID_IMPLEMENT_LIMITS); -+ -+ if (hv_common_init()) -+ return; -+ -+ guest_id = hv_generate_guest_id(LINUX_VERSION_CODE); -+ wrmsrq(HV_X64_MSR_GUEST_OS_ID, guest_id); -+ -+ hv_hypercall_pg = __vmalloc_node_range(PAGE_SIZE, 1, MODULES_VADDR, -+ MODULES_END, GFP_KERNEL, PAGE_KERNEL_ROX, -+ VM_FLUSH_RESET_PERMS, NUMA_NO_NODE, -+ __builtin_return_address(0)); -+ if (!hv_hypercall_pg) -+ goto clean_guest_os_id; -+ -+ rdmsrq(HV_X64_MSR_HYPERCALL, hypercall_msr.as_uint64); -+ hypercall_msr.enable = 1; -+ /* -+ * Hand Xen the machine frame so it fills the correct page: for a PV -+ * guest the pseudo-physical PFN differs from the machine (L1-physical) -+ * frame the hypervisor uses. -+ */ -+ hypercall_msr.guest_physical_address = -+ hv_nested_hostpfn(vmalloc_to_pfn(hv_hypercall_pg)); -+ wrmsrq(HV_X64_MSR_HYPERCALL, hypercall_msr.as_uint64); -+ hv_set_hypercall_pg(hv_hypercall_pg); -+ -+ /* -+ * Bring up the per-cpu input pages (VP assist is left NULL: it is a -+ * lazy-EOI optimisation the PV dom0 does not need). -+ */ -+ if (cpuhp_setup_state(CPUHP_AP_HYPERV_ONLINE, "x86/hyperv_init:online", -+ hv_cpu_init, hv_cpu_die) < 0) -+ goto clean_hypercall_pg; -+ -+ return; -+ -+clean_hypercall_pg: -+ hv_set_hypercall_pg(NULL); -+ vfree(hv_hypercall_pg); -+ hv_hypercall_pg = NULL; -+clean_guest_os_id: -+ wrmsrq(HV_X64_MSR_GUEST_OS_ID, 0); -+ hyperv_nested_on_xen = false; -+ hv_common_free(); -+} -+#endif /* CONFIG_XEN_PV */ -+ - /* - * This routine is called before kexec/kdump, it does the required cleanup. - */ -@@ -675,9 +764,10 @@ bool hv_is_hyperv_initialized(void) - - /* - * Ensure that we're really on Hyper-V, and not a KVM or Xen -- * emulation of Hyper-V -+ * emulation of Hyper-V. The exception is a Xen PV dom0 nested under -+ * Hyper-V, which drives VMBus with Xen proxying the enlightenments. - */ -- if (x86_hyper_type != X86_HYPER_MS_HYPERV) -+ if (x86_hyper_type != X86_HYPER_MS_HYPERV && !hyperv_nested_on_xen) - return false; - - /* A TDX VM with no paravisor uses TDX GHCI call rather than hv_hypercall_pg */ -diff --git a/arch/x86/include/asm/mshyperv.h b/arch/x86/include/asm/mshyperv.h -index 605abd021..79a303ace 100644 ---- a/arch/x86/include/asm/mshyperv.h -+++ b/arch/x86/include/asm/mshyperv.h -@@ -63,14 +63,40 @@ DECLARE_STATIC_CALL(hv_hypercall, hv_std_hypercall); - #define HV_AP_INIT_GPAT_DEFAULT 0x0007040600070406ULL - #define HV_AP_SEGMENT_LIMIT 0xffffffff - -+/* -+ * Nested Hyper-V-on-Xen host-address translation. Shared include-guard with -+ * asm-generic/mshyperv.h so this is defined exactly once regardless of include -+ * order/arch. A Xen PV dom0 nested under Hyper-V must hand the host machine -+ * (L1-physical) frames, not its own pseudo-physical frames. -+ */ -+#ifndef __HV_NESTED_ON_XEN_DEFINED -+#define __HV_NESTED_ON_XEN_DEFINED -+#ifdef CONFIG_XEN_PV -+extern bool hyperv_nested_on_xen; -+unsigned long hv_nested_hostpfn(unsigned long pfn); -+u64 hv_nested_hostpa(void *va); -+void __init hyperv_init_nested_on_xen(void); -+int hyperv_setup_xen_vmbus_irq(void (*isr)(void)); -+#else -+#define hyperv_nested_on_xen false -+/* Never called (the caller's hyperv_nested_on_xen test is a build-time false). */ -+static inline unsigned long hv_nested_hostpfn(unsigned long pfn) { return pfn; } -+static inline u64 hv_nested_hostpa(void *va) { return 0; } -+static inline void hyperv_init_nested_on_xen(void) {} -+static inline int hyperv_setup_xen_vmbus_irq(void (*isr)(void)) { return -ENODEV; } -+#endif -+#endif /* __HV_NESTED_ON_XEN_DEFINED */ -+ - /* - * If the hypercall involves no input or output parameters, the hypervisor - * ignores the corresponding GPA pointer. - */ - static inline u64 hv_do_hypercall(u64 control, void *input, void *output) - { -- u64 input_address = input ? virt_to_phys(input) : 0; -- u64 output_address = output ? virt_to_phys(output) : 0; -+ u64 input_address = input ? -+ (hyperv_nested_on_xen ? hv_nested_hostpa(input) : virt_to_phys(input)) : 0; -+ u64 output_address = output ? -+ (hyperv_nested_on_xen ? hv_nested_hostpa(output) : virt_to_phys(output)) : 0; - - #ifdef CONFIG_X86_64 - return static_call_mod(hv_hypercall)(control, input_address, output_address); -diff --git a/arch/x86/xen/Makefile b/arch/x86/xen/Makefile -index a9ec8c9f5..58484ae6a 100644 ---- a/arch/x86/xen/Makefile -+++ b/arch/x86/xen/Makefile -@@ -35,6 +35,8 @@ obj-$(CONFIG_XEN_PV) += xen-asm.o - - obj-$(CONFIG_XEN_PVH) += enlighten_pvh.o - -+obj-$(CONFIG_XEN_PV) += hyperv.o -+ - obj-$(CONFIG_EVENT_TRACING) += trace.o - - obj-$(CONFIG_SMP) += smp.o -diff --git a/arch/x86/xen/hyperv.c b/arch/x86/xen/hyperv.c -new file mode 100644 -index 000000000..be66bb220 ---- /dev/null -+++ b/arch/x86/xen/hyperv.c -@@ -0,0 +1,144 @@ -+// SPDX-License-Identifier: GPL-2.0 -+/* -+ * Xen PV dom0 nested under Hyper-V. -+ * -+ * When Xen itself runs as a guest of Hyper-V (e.g. a nested VM on Azure), its -+ * PV dom0 must drive the L0 host's synthetic (VMBus) devices for storage and -+ * networking. Xen exposes the Hyper-V enlightenments to dom0 and proxies the -+ * hypercalls / synthetic interrupts, so here we detect that situation, bring up -+ * the (otherwise Hyper-V-only) VMBus stack while remaining a Xen guest, and -+ * route the relayed VMBus interrupt (delivered by Xen as VIRQ_HYPERV_VMBUS) -+ * into the VMBus ISR. -+ */ -+#include -+ -+#if IS_ENABLED(CONFIG_HYPERV) -+ -+#include -+#include -+#include -+#include -+ -+#include -+#include -+#include -+ -+#include -+#include -+#include -+ -+/* -+ * Translate a guest pseudo-physical frame / address to the machine -+ * (L1-physical) frame the underlying Hyper-V host must use. For a Xen PV dom0 -+ * the two differ, so every host-facing VMBus / SynIC / hypercall address must -+ * be run through these. -+ */ -+unsigned long hv_nested_hostpfn(unsigned long pfn) -+{ -+ return hyperv_nested_on_xen ? pfn_to_mfn(pfn) : pfn; -+} -+EXPORT_SYMBOL_GPL(hv_nested_hostpfn); -+ -+u64 hv_nested_hostpa(void *va) -+{ -+ unsigned long pfn = virt_to_pfn(va); -+ -+ if (hyperv_nested_on_xen) -+ pfn = pfn_to_mfn(pfn); -+ -+ return ((u64)pfn << PAGE_SHIFT) | offset_in_page(va); -+} -+EXPORT_SYMBOL_GPL(hv_nested_hostpa); -+ -+static bool __init xen_detect_hyperv_l0(void) -+{ -+ u32 eax, ebx, ecx, edx; -+ -+ if (!xen_pv_domain()) -+ return false; -+ -+ /* -+ * Xen presents the underlying host's "Microsoft Hv" leaves at -+ * 0x40000000 (its own XenVMMXenVMM leaves are relocated to 0x40000100). -+ */ -+ cpuid(HYPERV_CPUID_VENDOR_AND_MAX_FUNCTIONS, &eax, &ebx, &ecx, &edx); -+ -+ return ebx == 0x7263694d && /* "Micr" */ -+ ecx == 0x666f736f && /* "osof" */ -+ edx == 0x76482074; /* "t Hv" */ -+} -+ -+static int __init xen_hyperv_init(void) -+{ -+ if (!xen_detect_hyperv_l0()) -+ return 0; -+ -+ pr_info("Xen: nested under Hyper-V, enabling VMBus passthrough\n"); -+ hyperv_nested_on_xen = true; -+ hyperv_init_nested_on_xen(); -+ -+ return 0; -+} -+arch_initcall(xen_hyperv_init); -+ -+/* -+ * VMBus interrupt bridge. A PV dom0 receives interrupts as Xen event channels, -+ * not as the native SINT vector VMBus programs, so Xen relays each VMBus -+ * message via the per-vcpu VIRQ_HYPERV_VMBUS. Bind it on every cpu and drive -+ * the VMBus ISR from its handler. -+ */ -+static void (*xen_vmbus_handler)(void); -+static DEFINE_PER_CPU(int, xen_vmbus_irq); -+ -+static irqreturn_t xen_hyperv_vmbus_isr(int irq, void *dev_id) -+{ -+ if (xen_vmbus_handler) -+ xen_vmbus_handler(); -+ -+ return IRQ_HANDLED; -+} -+ -+static int xen_hyperv_vmbus_cpu_up(unsigned int cpu) -+{ -+ int irq; -+ -+ irq = bind_virq_to_irqhandler(VIRQ_HYPERV_VMBUS, cpu, -+ xen_hyperv_vmbus_isr, -+ IRQF_PERCPU | IRQF_NOBALANCING, -+ "hyperv-vmbus", NULL); -+ if (irq < 0) -+ return irq; -+ -+ per_cpu(xen_vmbus_irq, cpu) = irq; -+ -+ return 0; -+} -+ -+static int xen_hyperv_vmbus_cpu_down(unsigned int cpu) -+{ -+ int irq = per_cpu(xen_vmbus_irq, cpu); -+ -+ if (irq > 0) { -+ unbind_from_irqhandler(irq, NULL); -+ per_cpu(xen_vmbus_irq, cpu) = 0; -+ } -+ -+ return 0; -+} -+ -+int hyperv_setup_xen_vmbus_irq(void (*isr)(void)) -+{ -+ int ret; -+ -+ if (!hyperv_nested_on_xen) -+ return -ENODEV; -+ -+ xen_vmbus_handler = isr; -+ -+ ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "xen/hyperv-vmbus:online", -+ xen_hyperv_vmbus_cpu_up, xen_hyperv_vmbus_cpu_down); -+ -+ return ret < 0 ? ret : 0; -+} -+ -+#endif /* CONFIG_HYPERV */ -diff --git a/include/asm-generic/mshyperv.h b/include/asm-generic/mshyperv.h -index b89c7e3a2..23728aff0 100644 ---- a/include/asm-generic/mshyperv.h -+++ b/include/asm-generic/mshyperv.h -@@ -363,6 +363,28 @@ static inline enum hv_isolation_type hv_get_isolation_type(void) - } - #endif /* CONFIG_HYPERV */ - -+/* -+ * Nested Hyper-V-on-Xen host-address translation. Shared include-guard with -+ * arch/x86/include/asm/mshyperv.h (which defines this first on x86, ahead of -+ * hv_do_hypercall()); this copy serves other arches. -+ */ -+#ifndef __HV_NESTED_ON_XEN_DEFINED -+#define __HV_NESTED_ON_XEN_DEFINED -+#ifdef CONFIG_XEN_PV -+extern bool hyperv_nested_on_xen; -+unsigned long hv_nested_hostpfn(unsigned long pfn); -+u64 hv_nested_hostpa(void *va); -+void __init hyperv_init_nested_on_xen(void); -+int hyperv_setup_xen_vmbus_irq(void (*isr)(void)); -+#else -+#define hyperv_nested_on_xen false -+static inline unsigned long hv_nested_hostpfn(unsigned long pfn) { return pfn; } -+static inline u64 hv_nested_hostpa(void *va) { return 0; } -+static inline void hyperv_init_nested_on_xen(void) {} -+static inline int hyperv_setup_xen_vmbus_irq(void (*isr)(void)) { return -ENODEV; } -+#endif -+#endif /* __HV_NESTED_ON_XEN_DEFINED */ -+ - #if IS_ENABLED(CONFIG_MSHV_ROOT) - static inline bool hv_root_partition(void) - { -diff --git a/include/xen/interface/xen.h b/include/xen/interface/xen.h -index 0ca23eca2..a7687ad86 100644 ---- a/include/xen/interface/xen.h -+++ b/include/xen/interface/xen.h -@@ -102,6 +102,13 @@ - /* Architecture-specific VIRQ definitions. */ - #define VIRQ_ARCH_0 16 - #define VIRQ_ARCH_1 17 -+/* -+ * Relayed Hyper-V VMBus synthetic interrupt, for a Xen dom0 running nested -+ * under Hyper-V: Xen owns the L0 SynIC and forwards VMBus messages to dom0 via -+ * this per-vcpu VIRQ. Must match the Xen definition. NB: VIRQ_ARCH_0 is -+ * VIRQ_MCA (used by xen_mcelog), so VMBus uses VIRQ_ARCH_1. -+ */ -+#define VIRQ_HYPERV_VMBUS VIRQ_ARCH_1 - #define VIRQ_ARCH_2 18 - #define VIRQ_ARCH_3 19 - #define VIRQ_ARCH_4 20 --- -2.54.0 - diff --git a/patches/hyperv/0002-Drivers-hv-use-host-frames-for-GPADLs-and-SynIC-page.patch b/patches/hyperv/0002-Drivers-hv-use-host-frames-for-GPADLs-and-SynIC-page.patch deleted file mode 100644 index db435247..00000000 --- a/patches/hyperv/0002-Drivers-hv-use-host-frames-for-GPADLs-and-SynIC-page.patch +++ /dev/null @@ -1,99 +0,0 @@ -From b56bf4ed14ef96ca1a9f5c5b90fe6e31cd0bcd96 Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Thu, 23 Jul 2026 15:42:06 -0700 -Subject: [PATCH 2/5] Drivers: hv: use host frames for GPADLs and SynIC pages - when nested on Xen - -The VMBus GPADL page lists and the SynIC SIMP/SIEFP pages must hold the -machine (L1-physical) frames the L0 Hyper-V host can access, not dom0's -pseudo-physical frames. Route them through hv_nested_hostpfn()/ -hv_nested_hostpa() when running nested on Xen; the translation is a -no-op otherwise. - -Signed-off-by: Ariadne Conill ---- - drivers/hv/channel.c | 7 ++++++- - drivers/hv/connection.c | 15 +++++++++++---- - drivers/hv/hv.c | 8 ++++---- - 3 files changed, 21 insertions(+), 9 deletions(-) - -diff --git a/drivers/hv/channel.c b/drivers/hv/channel.c -index 162d6aeec..c2af4fa5a 100644 ---- a/drivers/hv/channel.c -+++ b/drivers/hv/channel.c -@@ -114,7 +114,12 @@ static inline u64 hv_gpadl_hvpfn(enum hv_gpadl_type type, void *kbuffer, - break; - } - -- return virt_to_hvpfn(kbuffer + delta + (HV_HYP_PAGE_SIZE * i)); -+ /* -+ * The host consumes these as machine (L1-physical) frames; on a Xen PV -+ * dom0 that differs from the guest pseudo-physical frame. -+ */ -+ return hv_nested_hostpfn(virt_to_hvpfn(kbuffer + delta + -+ (HV_HYP_PAGE_SIZE * i))); - } - - /* -diff --git a/drivers/hv/connection.c b/drivers/hv/connection.c -index 1fe3573ae..09f7d6325 100644 ---- a/drivers/hv/connection.c -+++ b/drivers/hv/connection.c -@@ -101,17 +101,24 @@ int vmbus_negotiate_version(struct vmbus_channel_msginfo *msginfo, u32 version) - msg->msg_vtl = ms_hyperv.vtl; - vmbus_connection.msg_conn_id = VMBUS_MESSAGE_CONNECTION_ID_4; - } else { -- msg->interrupt_page = virt_to_phys(vmbus_connection.int_page); -+ msg->interrupt_page = hyperv_nested_on_xen ? -+ hv_nested_hostpa(vmbus_connection.int_page) : -+ virt_to_phys(vmbus_connection.int_page); - vmbus_connection.msg_conn_id = VMBUS_MESSAGE_CONNECTION_ID; - } - - /* - * shared_gpa_boundary is zero in non-SNP VMs, so it's safe to always -- * bitwise OR it -+ * bitwise OR it. On a Xen PV dom0 the host needs machine (L1-physical) -+ * addresses for the monitor pages. - */ -- msg->monitor_page1 = virt_to_phys(vmbus_connection.monitor_pages[0]) | -+ msg->monitor_page1 = (hyperv_nested_on_xen ? -+ hv_nested_hostpa(vmbus_connection.monitor_pages[0]) : -+ virt_to_phys(vmbus_connection.monitor_pages[0])) | - ms_hyperv.shared_gpa_boundary; -- msg->monitor_page2 = virt_to_phys(vmbus_connection.monitor_pages[1]) | -+ msg->monitor_page2 = (hyperv_nested_on_xen ? -+ hv_nested_hostpa(vmbus_connection.monitor_pages[1]) : -+ virt_to_phys(vmbus_connection.monitor_pages[1])) | - ms_hyperv.shared_gpa_boundary; - - msg->target_vcpu = hv_cpu_number_to_vp_number(VMBUS_CONNECT_CPU); -diff --git a/drivers/hv/hv.c b/drivers/hv/hv.c -index b14c5f9e0..6af9abc5b 100644 ---- a/drivers/hv/hv.c -+++ b/drivers/hv/hv.c -@@ -283,8 +283,8 @@ void hv_synic_enable_regs(unsigned int cpu) - if (!hv_cpu->hyp_synic_message_page) - pr_err("Fail to map synic message page.\n"); - } else { -- simp.base_simp_gpa = virt_to_phys(hv_cpu->hyp_synic_message_page) -- >> HV_HYP_PAGE_SHIFT; -+ simp.base_simp_gpa = hv_nested_hostpfn( -+ virt_to_phys(hv_cpu->hyp_synic_message_page) >> HV_HYP_PAGE_SHIFT); - } - - hv_set_msr(HV_MSR_SIMP, simp.as_uint64); -@@ -302,8 +302,8 @@ void hv_synic_enable_regs(unsigned int cpu) - if (!hv_cpu->hyp_synic_event_page) - pr_err("Fail to map synic event page.\n"); - } else { -- siefp.base_siefp_gpa = virt_to_phys(hv_cpu->hyp_synic_event_page) -- >> HV_HYP_PAGE_SHIFT; -+ siefp.base_siefp_gpa = hv_nested_hostpfn( -+ virt_to_phys(hv_cpu->hyp_synic_event_page) >> HV_HYP_PAGE_SHIFT); - } - - hv_set_msr(HV_MSR_SIEFP, siefp.as_uint64); --- -2.54.0 - diff --git a/patches/hyperv/0003-drivers-hv-deliver-the-VMBus-interrupt-via-a-Xen-VIR.patch b/patches/hyperv/0003-drivers-hv-deliver-the-VMBus-interrupt-via-a-Xen-VIR.patch deleted file mode 100644 index 612d2fd3..00000000 --- a/patches/hyperv/0003-drivers-hv-deliver-the-VMBus-interrupt-via-a-Xen-VIR.patch +++ /dev/null @@ -1,43 +0,0 @@ -From f3ba01a01917a88fb48b59583200cc4ae09acc14 Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Thu, 23 Jul 2026 15:42:07 -0700 -Subject: [PATCH 3/5] drivers: hv: deliver the VMBus interrupt via a Xen VIRQ - when nested on Xen - -When nested on Xen the host's synthetic interrupt is taken by Xen and -relayed to dom0 as VIRQ_HYPERV_VMBUS. Bind the VMBus ISR to that VIRQ -instead of the Hyper-V synthetic vector in that case. - -Signed-off-by: Ariadne Conill ---- - drivers/hv/vmbus_drv.c | 14 +++++++++++++- - 1 file changed, 13 insertions(+), 1 deletion(-) - -diff --git a/drivers/hv/vmbus_drv.c b/drivers/hv/vmbus_drv.c -index 1a0e350c1..784b4a59e 100644 ---- a/drivers/hv/vmbus_drv.c -+++ b/drivers/hv/vmbus_drv.c -@@ -1429,7 +1429,19 @@ static int vmbus_bus_init(void) - vmbus_irq_initialized = true; - } - -- if (vmbus_irq == -1) { -+ if (hyperv_nested_on_xen) { -+ /* -+ * A Xen PV dom0 nested under Hyper-V receives the relayed VMBus -+ * interrupt as a Xen event channel (VIRQ_HYPERV_VMBUS), not as -+ * the native SINT vector, so use the Xen-side bridge. -+ */ -+ ret = hyperv_setup_xen_vmbus_irq(vmbus_isr); -+ if (ret) { -+ pr_err("Can't set up Xen VMbus interrupt bridge, Err %d\n", -+ ret); -+ goto err_setup; -+ } -+ } else if (vmbus_irq == -1) { - hv_setup_vmbus_handler(vmbus_isr); - } else { - vmbus_evt = alloc_percpu(long); --- -2.54.0 - diff --git a/patches/hyperv/0004-hv_netvsc-DMA-map-transmit-pages-when-nested-on-Xen.patch b/patches/hyperv/0004-hv_netvsc-DMA-map-transmit-pages-when-nested-on-Xen.patch deleted file mode 100644 index 4b18f0d5..00000000 --- a/patches/hyperv/0004-hv_netvsc-DMA-map-transmit-pages-when-nested-on-Xen.patch +++ /dev/null @@ -1,46 +0,0 @@ -From 087e9ab69e40bcc54561aa6c51a4b314b56a693f Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Thu, 23 Jul 2026 15:42:07 -0700 -Subject: [PATCH 4/5] hv_netvsc: DMA-map transmit pages when nested on Xen - -Like an isolation VM, a Xen PV dom0 nested under Hyper-V must hand the -host machine (bus) addresses for transmit page buffers. Extend the -DMA-map/unmap path (which routes through swiotlb-xen) to also run in the -nested-on-Xen case, so RNDIS control and data sends reference machine -frames the host can read. - -Signed-off-by: Ariadne Conill ---- - drivers/net/hyperv/netvsc.c | 9 +++++++-- - 1 file changed, 7 insertions(+), 2 deletions(-) - -diff --git a/drivers/net/hyperv/netvsc.c b/drivers/net/hyperv/netvsc.c -index 073f2b7bd..fab91c7ab 100644 ---- a/drivers/net/hyperv/netvsc.c -+++ b/drivers/net/hyperv/netvsc.c -@@ -993,7 +993,7 @@ void netvsc_dma_unmap(struct hv_device *hv_dev, - { - int i; - -- if (!hv_is_isolation_supported()) -+ if (!hv_is_isolation_supported() && !hyperv_nested_on_xen) - return; - - if (!packet->dma_range) -@@ -1033,7 +1033,12 @@ static int netvsc_dma_map(struct hv_device *hv_dev, - dma_addr_t dma; - int i; - -- if (!hv_is_isolation_supported()) -+ /* -+ * A Xen PV dom0 nested under Hyper-V must also DMA-map packet pages so -+ * the host receives machine (bus) addresses (via swiotlb-xen), not the -+ * guest's pseudo-physical PFNs. -+ */ -+ if (!hv_is_isolation_supported() && !hyperv_nested_on_xen) - return 0; - - packet->dma_range = kcalloc(page_count, --- -2.54.0 - diff --git a/patches/hyperv/0005-x86-hyperv-take-vPCI-device-interrupts-through-Xen-w.patch b/patches/hyperv/0005-x86-hyperv-take-vPCI-device-interrupts-through-Xen-w.patch deleted file mode 100644 index bf4652a3..00000000 --- a/patches/hyperv/0005-x86-hyperv-take-vPCI-device-interrupts-through-Xen-w.patch +++ /dev/null @@ -1,513 +0,0 @@ -From c98cf28aec3bc8a65b872834d2e30deb7c18f9a4 Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Mon, 27 Jul 2026 20:30:58 -0700 -Subject: [PATCH 5/5] x86/hyperv: take vPCI device interrupts through Xen when - nested on Xen - -A Xen PV dom0 nested under Hyper-V cannot back vPCI MSIs with -x86_vector_domain. The host composes them against a vector on a virtual -processor belonging to Xen, so Xen owns the vector and tells us which -one; and PV never onlines the local APIC, so cpu0 is the only CPU in the -vector matrix and acking through apic_ack_edge() reaches xen_apic_eoi(), -which warns. - -Query that vector and supply a minimal parent irq domain for these -interrupts, one in which masking and affinity belong to the host: Xen -has already acked the vector by the time it relays the interrupt as -VIRQ_HYPERV_VPCI, and affinity takes effect through the retarget -hypercall the vPCI irq_chip above issues on unmask. As one vector -serves every vPCI device, run each registered handler on every relayed -interrupt, and mark the interrupts polled so that the spurious detector -does not eventually disable a busy device whose handler usually has -nothing to do. - -Multi-message MSI cannot work in this configuration - a contiguous block -of vectors cannot come from the single relay vector - so drop it; MSI-X, -which NVMe and MANA use, is unaffected. Config space likewise takes the -trapped MMIO path in preference to HVCALL_MMIO_READ/WRITE, whose output -page Xen would otherwise have to marshal, as arm64 already does. - -Signed-off-by: Ariadne Conill ---- - arch/x86/include/asm/mshyperv.h | 10 + - arch/x86/xen/hyperv.c | 275 ++++++++++++++++++++++++++++ - drivers/pci/controller/pci-hyperv.c | 35 +++- - include/asm-generic/mshyperv.h | 5 + - include/xen/interface/physdev.h | 14 ++ - include/xen/interface/xen.h | 8 + - 6 files changed, 345 insertions(+), 2 deletions(-) - -diff --git a/arch/x86/include/asm/mshyperv.h b/arch/x86/include/asm/mshyperv.h -index 79a303ace..f0f402f34 100644 ---- a/arch/x86/include/asm/mshyperv.h -+++ b/arch/x86/include/asm/mshyperv.h -@@ -71,12 +71,20 @@ DECLARE_STATIC_CALL(hv_hypercall, hv_std_hypercall); - */ - #ifndef __HV_NESTED_ON_XEN_DEFINED - #define __HV_NESTED_ON_XEN_DEFINED -+struct irq_domain; - #ifdef CONFIG_XEN_PV - extern bool hyperv_nested_on_xen; - unsigned long hv_nested_hostpfn(unsigned long pfn); - u64 hv_nested_hostpa(void *va); - void __init hyperv_init_nested_on_xen(void); - int hyperv_setup_xen_vmbus_irq(void (*isr)(void)); -+/* -+ * The vector vPCI device MSIs must be composed with, and the parent irq domain -+ * for them: the host delivers such an MSI to a vector Xen owns and relays it to -+ * us, so x86_vector_domain's vectors (meaningless to a PV guest) cannot be used. -+ */ -+unsigned int hyperv_xen_vpci_vector(void); -+struct irq_domain *hyperv_xen_vpci_root_domain(void); - #else - #define hyperv_nested_on_xen false - /* Never called (the caller's hyperv_nested_on_xen test is a build-time false). */ -@@ -84,6 +92,8 @@ static inline unsigned long hv_nested_hostpfn(unsigned long pfn) { return pfn; } - static inline u64 hv_nested_hostpa(void *va) { return 0; } - static inline void hyperv_init_nested_on_xen(void) {} - static inline int hyperv_setup_xen_vmbus_irq(void (*isr)(void)) { return -ENODEV; } -+static inline unsigned int hyperv_xen_vpci_vector(void) { return 0; } -+static inline struct irq_domain *hyperv_xen_vpci_root_domain(void) { return NULL; } - #endif - #endif /* __HV_NESTED_ON_XEN_DEFINED */ - -diff --git a/arch/x86/xen/hyperv.c b/arch/x86/xen/hyperv.c -index be66bb220..dc6c8b4b5 100644 ---- a/arch/x86/xen/hyperv.c -+++ b/arch/x86/xen/hyperv.c -@@ -16,15 +16,22 @@ - - #include - #include -+#include -+#include - #include -+#include - #include -+#include -+#include - - #include - #include -+#include - #include - - #include - #include -+#include - #include - - /* -@@ -141,4 +148,272 @@ int hyperv_setup_xen_vmbus_irq(void (*isr)(void)) - return ret < 0 ? ret : 0; - } - -+/* -+ * vPCI (VMBus-assigned PCI device, e.g. NVMe or a MANA VF) interrupt bridge. -+ * -+ * The host composes these MSIs itself and delivers them to the VP, which under -+ * Xen belongs to Xen and not to us: the vPCI protocol messages that request them -+ * travel in a VMBus ring buffer Xen does not inspect, so it cannot rewrite the -+ * vector we ask for. Xen therefore owns one vector for all of them, tells us -+ * which (PHYSDEVOP_hyperv_vpci_vector), and relays each delivery to the vcpu -+ * that received it as VIRQ_HYPERV_VPCI. We compose every vPCI MSI with that -+ * vector and demultiplex here, since Xen cannot tell the devices apart. -+ * -+ * That also means x86_vector_domain cannot be the parent of the vPCI MSI domain: -+ * its vectors are meaningless to a PV guest (and acking one reaches -+ * xen_apic_eoi(), which warns). We supply a minimal parent domain instead - the -+ * host owns masking and affinity, and Xen has already EOId the vector by the -+ * time we run. -+ */ -+static u32 xen_hv_vpci_vector; -+static struct irq_domain *xen_hv_vpci_domain; -+static DEFINE_PER_CPU(int, xen_vpci_irq); -+ -+/* The vPCI interrupts to consider on each relayed VIRQ. */ -+struct xen_hv_vpci_irq { -+ struct list_head node; -+ struct rcu_head rcu; -+ unsigned int irq; -+}; -+ -+static LIST_HEAD(xen_hv_vpci_irqs); -+static DEFINE_SPINLOCK(xen_hv_vpci_irqs_lock); -+ -+static int xen_hv_vpci_irq_add(unsigned int irq) -+{ -+ struct xen_hv_vpci_irq *entry; -+ -+ entry = kzalloc(sizeof(*entry), GFP_KERNEL); -+ if (!entry) -+ return -ENOMEM; -+ -+ entry->irq = irq; -+ -+ spin_lock(&xen_hv_vpci_irqs_lock); -+ list_add_rcu(&entry->node, &xen_hv_vpci_irqs); -+ spin_unlock(&xen_hv_vpci_irqs_lock); -+ -+ return 0; -+} -+ -+static void xen_hv_vpci_irq_del(unsigned int irq) -+{ -+ struct xen_hv_vpci_irq *entry; -+ -+ spin_lock(&xen_hv_vpci_irqs_lock); -+ list_for_each_entry(entry, &xen_hv_vpci_irqs, node) { -+ if (entry->irq == irq) { -+ list_del_rcu(&entry->node); -+ kfree_rcu(entry, rcu); -+ break; -+ } -+ } -+ spin_unlock(&xen_hv_vpci_irqs_lock); -+} -+ -+static irqreturn_t xen_hyperv_vpci_isr(int irq, void *dev_id) -+{ -+ struct xen_hv_vpci_irq *entry; -+ -+ /* -+ * One or more of these devices interrupted; which one is not -+ * recoverable from the relay, so run them all. Device handlers are -+ * required to tolerate a spurious call. -+ */ -+ rcu_read_lock(); -+ list_for_each_entry_rcu(entry, &xen_hv_vpci_irqs, node) -+ generic_handle_irq(entry->irq); -+ rcu_read_unlock(); -+ -+ return IRQ_HANDLED; -+} -+ -+static int xen_hyperv_vpci_cpu_up(unsigned int cpu) -+{ -+ int irq; -+ -+ irq = bind_virq_to_irqhandler(VIRQ_HYPERV_VPCI, cpu, -+ xen_hyperv_vpci_isr, -+ IRQF_PERCPU | IRQF_NOBALANCING, -+ "hyperv-vpci", NULL); -+ if (irq < 0) -+ return irq; -+ -+ per_cpu(xen_vpci_irq, cpu) = irq; -+ -+ return 0; -+} -+ -+static int xen_hyperv_vpci_cpu_down(unsigned int cpu) -+{ -+ int irq = per_cpu(xen_vpci_irq, cpu); -+ -+ if (irq > 0) { -+ unbind_from_irqhandler(irq, NULL); -+ per_cpu(xen_vpci_irq, cpu) = 0; -+ } -+ -+ return 0; -+} -+ -+/* -+ * Xen acknowledged the host's vector before relaying, so there is nothing to ack -+ * here beyond completing a pending affinity change - the same thing -+ * apic_ack_edge() does for a native MSI. -+ */ -+static void xen_hv_vpci_irq_ack(struct irq_data *data) -+{ -+ irq_move_irq(data); -+} -+ -+static void xen_hv_vpci_irq_noop(struct irq_data *data) -+{ -+} -+ -+/* -+ * Record where the interrupt should land; the vPCI irq_chip above us turns this -+ * into a HVCALL_RETARGET_INTERRUPT (proxied by Xen) when it unmasks, which is -+ * what actually moves it. -+ */ -+static int xen_hv_vpci_set_affinity(struct irq_data *data, -+ const struct cpumask *mask, bool force) -+{ -+ unsigned int cpu = cpumask_first_and(mask, cpu_online_mask); -+ -+ if (cpu >= nr_cpu_ids) -+ return -EINVAL; -+ -+ irq_data_update_effective_affinity(data, cpumask_of(cpu)); -+ -+ return IRQ_SET_MASK_OK; -+} -+ -+static struct irq_chip xen_hv_vpci_irq_chip = { -+ .name = "XEN-HV-vPCI", -+ .irq_ack = xen_hv_vpci_irq_ack, -+ .irq_eoi = xen_hv_vpci_irq_noop, -+ .irq_mask = xen_hv_vpci_irq_noop, -+ .irq_unmask = xen_hv_vpci_irq_noop, -+ .irq_set_affinity = xen_hv_vpci_set_affinity, -+}; -+ -+static int xen_hv_vpci_domain_alloc(struct irq_domain *domain, unsigned int virq, -+ unsigned int nr_irqs, void *arg) -+{ -+ unsigned int i; -+ int ret; -+ -+ for (i = 0; i < nr_irqs; i++) { -+ ret = xen_hv_vpci_irq_add(virq + i); -+ if (ret) -+ goto err; -+ -+ irq_domain_set_info(domain, virq + i, virq + i, -+ &xen_hv_vpci_irq_chip, NULL, -+ handle_edge_irq, NULL, NULL); -+ -+ /* -+ * Every handler runs on every relayed interrupt, so most calls -+ * return IRQ_NONE by design. Tell the spurious-interrupt -+ * detector this line is polled, or it would eventually decide a -+ * busy device's interrupt is stuck and disable it. -+ */ -+ irq_set_status_flags(virq + i, IRQ_IS_POLLED); -+ -+ /* -+ * The host has to be given a VP to deliver to, and -+ * hv_compose_msi_msg() takes it from the effective affinity, so -+ * that must never be empty. -+ */ -+ irq_data_update_effective_affinity(irq_domain_get_irq_data(domain, -+ virq + i), -+ cpumask_of(0)); -+ } -+ -+ return 0; -+ -+err: -+ while (i--) -+ xen_hv_vpci_irq_del(virq + i); -+ -+ return ret; -+} -+ -+static void xen_hv_vpci_domain_free(struct irq_domain *domain, unsigned int virq, -+ unsigned int nr_irqs) -+{ -+ unsigned int i; -+ -+ for (i = 0; i < nr_irqs; i++) { -+ xen_hv_vpci_irq_del(virq + i); -+ irq_domain_reset_irq_data(irq_domain_get_irq_data(domain, virq + i)); -+ } -+} -+ -+static const struct irq_domain_ops xen_hv_vpci_domain_ops = { -+ .alloc = xen_hv_vpci_domain_alloc, -+ .free = xen_hv_vpci_domain_free, -+}; -+ -+/* The vector every vPCI MSI must be composed with. */ -+unsigned int hyperv_xen_vpci_vector(void) -+{ -+ return xen_hv_vpci_vector; -+} -+EXPORT_SYMBOL_GPL(hyperv_xen_vpci_vector); -+ -+/* -+ * Parent irq domain for vPCI MSIs, brought up on first use. Returns NULL if Xen -+ * is not relaying vPCI interrupts, which leaves the vPCI bus unusable - the -+ * devices on it cannot deliver an interrupt any other way. -+ */ -+struct irq_domain *hyperv_xen_vpci_root_domain(void) -+{ -+ static DEFINE_MUTEX(setup_lock); -+ struct physdev_hyperv_vpci_vector out = {}; -+ struct fwnode_handle *fn; -+ int ret; -+ -+ if (!hyperv_nested_on_xen) -+ return NULL; -+ -+ guard(mutex)(&setup_lock); -+ -+ if (xen_hv_vpci_domain) -+ return xen_hv_vpci_domain; -+ -+ ret = HYPERVISOR_physdev_op(PHYSDEVOP_hyperv_vpci_vector, &out); -+ if (ret) { -+ pr_err("Xen: no Hyper-V vPCI interrupt vector: %d\n", ret); -+ return NULL; -+ } -+ xen_hv_vpci_vector = out.vector; -+ -+ fn = irq_domain_alloc_named_fwnode("XEN-HV-vPCI"); -+ if (!fn) -+ return NULL; -+ -+ xen_hv_vpci_domain = irq_domain_create_tree(fn, &xen_hv_vpci_domain_ops, -+ NULL); -+ if (!xen_hv_vpci_domain) { -+ irq_domain_free_fwnode(fn); -+ return NULL; -+ } -+ -+ ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "xen/hyperv-vpci:online", -+ xen_hyperv_vpci_cpu_up, xen_hyperv_vpci_cpu_down); -+ if (ret < 0) { -+ pr_err("Xen: can't bind the Hyper-V vPCI VIRQ: %d\n", ret); -+ irq_domain_remove(xen_hv_vpci_domain); -+ xen_hv_vpci_domain = NULL; -+ return NULL; -+ } -+ -+ pr_info("Xen: Hyper-V vPCI interrupts relayed on vector 0x%x\n", -+ xen_hv_vpci_vector); -+ -+ return xen_hv_vpci_domain; -+} -+EXPORT_SYMBOL_GPL(hyperv_xen_vpci_root_domain); -+ - #endif /* CONFIG_HYPERV */ -diff --git a/drivers/pci/controller/pci-hyperv.c b/drivers/pci/controller/pci-hyperv.c -index 28b157297..1e95c2eaa 100644 ---- a/drivers/pci/controller/pci-hyperv.c -+++ b/drivers/pci/controller/pci-hyperv.c -@@ -586,12 +586,26 @@ static int hv_pci_irqchip_init(void) - - static struct irq_domain *hv_pci_get_root_domain(void) - { -+ /* -+ * Nested under Hyper-V in a Xen PV dom0 the host delivers these MSIs to -+ * a vector Xen owns, on a VP that is Xen's, so x86_vector_domain has -+ * nothing to allocate here - Xen provides the vector and the parent -+ * domain. See hyperv_xen_vpci_root_domain(). -+ */ -+ if (hyperv_nested_on_xen) -+ return hyperv_xen_vpci_root_domain(); -+ - return x86_vector_domain; - } - - static unsigned int hv_msi_get_int_vector(struct irq_data *data) - { -- struct irq_cfg *cfg = irqd_cfg(data); -+ struct irq_cfg *cfg; -+ -+ if (hyperv_nested_on_xen) -+ return hyperv_xen_vpci_vector(); -+ -+ cfg = irqd_cfg(data); - - return cfg->vector; - } -@@ -2124,6 +2138,15 @@ static bool hv_pcie_init_dev_msi_info(struct device *dev, struct irq_domain *dom - if (IS_ENABLED(CONFIG_X86)) - chip->flags |= IRQCHIP_MOVE_DEFERRED; - -+ /* -+ * Nested under Hyper-V in a Xen PV dom0 all vPCI MSIs share the single -+ * vector Xen relays on, so there is no contiguous block of vectors to -+ * back a multi-message MSI allocation. MSI-X (what NVMe and MANA use) -+ * is unaffected. -+ */ -+ if (hyperv_nested_on_xen) -+ info->flags &= ~MSI_FLAG_MULTI_PCI_MSI; -+ - return true; - } - -@@ -3806,7 +3829,15 @@ static int hv_pci_probe(struct hv_device *hdev, - hbus->bridge->domain_nr = dom; - #ifdef CONFIG_X86 - hbus->sysdata.domain = dom; -- hbus->use_calls = !!(ms_hyperv.hints & HV_X64_USE_MMIO_HYPERCALLS); -+ /* -+ * The MMIO hypercalls return data through an output page, which Xen -+ * would have to marshal on our behalf when nested in a PV dom0. Take -+ * the trapped-MMIO path instead (as arm64 does unconditionally): the -+ * config window's machine address is mapped directly, so the host -+ * intercepts the access with no hypervisor involvement. -+ */ -+ hbus->use_calls = !hyperv_nested_on_xen && -+ !!(ms_hyperv.hints & HV_X64_USE_MMIO_HYPERCALLS); - #elif defined(CONFIG_ARM64) - /* - * Set the PCI bus parent to be the corresponding VMbus -diff --git a/include/asm-generic/mshyperv.h b/include/asm-generic/mshyperv.h -index 23728aff0..85832b8e6 100644 ---- a/include/asm-generic/mshyperv.h -+++ b/include/asm-generic/mshyperv.h -@@ -370,18 +370,23 @@ static inline enum hv_isolation_type hv_get_isolation_type(void) - */ - #ifndef __HV_NESTED_ON_XEN_DEFINED - #define __HV_NESTED_ON_XEN_DEFINED -+struct irq_domain; - #ifdef CONFIG_XEN_PV - extern bool hyperv_nested_on_xen; - unsigned long hv_nested_hostpfn(unsigned long pfn); - u64 hv_nested_hostpa(void *va); - void __init hyperv_init_nested_on_xen(void); - int hyperv_setup_xen_vmbus_irq(void (*isr)(void)); -+unsigned int hyperv_xen_vpci_vector(void); -+struct irq_domain *hyperv_xen_vpci_root_domain(void); - #else - #define hyperv_nested_on_xen false - static inline unsigned long hv_nested_hostpfn(unsigned long pfn) { return pfn; } - static inline u64 hv_nested_hostpa(void *va) { return 0; } - static inline void hyperv_init_nested_on_xen(void) {} - static inline int hyperv_setup_xen_vmbus_irq(void (*isr)(void)) { return -ENODEV; } -+static inline unsigned int hyperv_xen_vpci_vector(void) { return 0; } -+static inline struct irq_domain *hyperv_xen_vpci_root_domain(void) { return NULL; } - #endif - #endif /* __HV_NESTED_ON_XEN_DEFINED */ - -diff --git a/include/xen/interface/physdev.h b/include/xen/interface/physdev.h -index df74e65a8..213919747 100644 ---- a/include/xen/interface/physdev.h -+++ b/include/xen/interface/physdev.h -@@ -296,6 +296,20 @@ struct physdev_dbgp_op { - } u; - }; - -+/* -+ * Query the interrupt vector Xen receives Hyper-V vPCI (VMBus-assigned PCI -+ * device) MSIs on, for a hardware domain running nested under Hyper-V. Such a -+ * device's MSI is composed by the host and delivered to the VP - which is Xen's, -+ * not ours - so we must compose every vPCI MSI with this vector and take the -+ * resulting interrupts as VIRQ_HYPERV_VPCI. Fails with -ENODEV if Xen is not -+ * relaying Hyper-V enlightenments to this domain. -+ */ -+#define PHYSDEVOP_hyperv_vpci_vector 33 -+struct physdev_hyperv_vpci_vector { -+ /* OUT */ -+ uint32_t vector; -+}; -+ - /* - * Notify that some PIRQ-bound event channels have been unmasked. - * ** This command is obsolete since interface version 0x00030202 and is ** -diff --git a/include/xen/interface/xen.h b/include/xen/interface/xen.h -index a7687ad86..812365302 100644 ---- a/include/xen/interface/xen.h -+++ b/include/xen/interface/xen.h -@@ -110,6 +110,14 @@ - */ - #define VIRQ_HYPERV_VMBUS VIRQ_ARCH_1 - #define VIRQ_ARCH_2 18 -+/* -+ * Relayed Hyper-V vPCI device interrupt, for the same nested dom0: the host -+ * delivers MSIs of VMBus-assigned PCI devices to a vector Xen owns (see -+ * PHYSDEVOP_hyperv_vpci_vector), and Xen notifies us via this per-vcpu VIRQ, -+ * leaving us to demultiplex among our own vPCI devices. Must match the Xen -+ * definition. -+ */ -+#define VIRQ_HYPERV_VPCI VIRQ_ARCH_2 - #define VIRQ_ARCH_3 19 - #define VIRQ_ARCH_4 20 - #define VIRQ_ARCH_5 21 --- -2.54.0 - diff --git a/patches/hyperv/0006-drivers-hv-refuse-to-balloon-when-nested-on-Xen.patch b/patches/hyperv/0006-drivers-hv-refuse-to-balloon-when-nested-on-Xen.patch deleted file mode 100644 index c861f97d..00000000 --- a/patches/hyperv/0006-drivers-hv-refuse-to-balloon-when-nested-on-Xen.patch +++ /dev/null @@ -1,50 +0,0 @@ -From a2a18f837ac9b7a127292afd34e28306b1110494 Mon Sep 17 00:00:00 2001 -From: Ariadne Conill -Date: Sat, 1 Aug 2026 12:26:13 -0700 -Subject: [PATCH 6/6] drivers: hv: refuse to balloon when nested on Xen - -The balloon tells the host which page frames it has withdrawn by frame -number, taken straight from page_to_pfn(). Those numbers are -pseudo-physical in a Xen PV dom0, so the host reads them as machine -frames and reclaims whatever Xen happens to have placed there - memory -belonging to dom0 itself or to another domain - which surfaces later as -pages of zeroes underneath unrelated processes. - -Translating the frame numbers would not make this correct. dom0's -memory is Xen's to hand back, and Xen has a balloon of its own for that; -a second one negotiating directly with the L0 host cannot be reconciled -with it. Decline to register the driver instead. - -Signed-off-by: Ariadne Conill ---- - drivers/hv/hv_balloon.c | 14 ++++++++++++++ - 1 file changed, 14 insertions(+) - -diff --git a/drivers/hv/hv_balloon.c b/drivers/hv/hv_balloon.c -index 2b4080e51..fa68fc759 100644 ---- a/drivers/hv/hv_balloon.c -+++ b/drivers/hv/hv_balloon.c -@@ -2131,6 +2131,20 @@ static struct hv_driver balloon_drv = { - - static int __init init_balloon_drv(void) - { -+ /* -+ * Dynamic memory cannot work when Linux is a Xen PV dom0 running -+ * nested under Hyper-V. Ballooning reports the page frames it has -+ * withdrawn directly to the host, but a PV guest's frame numbers are -+ * pseudo-physical; the host reads them as machine frames and reclaims -+ * whatever Xen placed there, silently destroying memory belonging to -+ * dom0 or to another domain. Translating them would not make this -+ * correct either: dom0's memory is Xen's to give back, not ours. -+ */ -+ if (hyperv_nested_on_xen) { -+ pr_info("nested under Xen, not registering: dynamic memory is unsupported\n"); -+ return -ENODEV; -+ } -+ - return vmbus_driver_register(&balloon_drv); - } - --- -2.54.0 - diff --git a/patches/openpax-6.10-20240913112548.patch b/patches/openpax-6.10-20240913112548.patch deleted file mode 100644 index f00813a8..00000000 --- a/patches/openpax-6.10-20240913112548.patch +++ /dev/null @@ -1,836 +0,0 @@ -diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt -index c82446cef8e2..fc9572f2c06c 100644 ---- a/Documentation/admin-guide/kernel-parameters.txt -+++ b/Documentation/admin-guide/kernel-parameters.txt -@@ -4388,6 +4388,9 @@ - the specified number of seconds. This is to be used if - your oopses keep scrolling off the screen. - -+ pax_softmode= -+ Enables OpenPaX soft mode if set to a non-zero value. -+ - pcbit= [HW,ISDN] - - pci=option[,option...] [PCI,EARLY] various PCI subsystem options. -diff --git a/arch/Kconfig b/arch/Kconfig -index 975dd22a2dbd..de69b8f5b5be 100644 ---- a/arch/Kconfig -+++ b/arch/Kconfig -@@ -1050,7 +1050,7 @@ config ARCH_MMAP_RND_BITS - int "Number of bits to use for ASLR of mmap base address" if EXPERT - range ARCH_MMAP_RND_BITS_MIN ARCH_MMAP_RND_BITS_MAX - default ARCH_MMAP_RND_BITS_DEFAULT if ARCH_MMAP_RND_BITS_DEFAULT -- default ARCH_MMAP_RND_BITS_MIN -+ default ARCH_MMAP_RND_BITS_MAX - depends on HAVE_ARCH_MMAP_RND_BITS - help - This value can be used to select the number of bits to use to -@@ -1084,7 +1084,7 @@ config ARCH_MMAP_RND_COMPAT_BITS - int "Number of bits to use for ASLR of mmap base address for compatible applications" if EXPERT - range ARCH_MMAP_RND_COMPAT_BITS_MIN ARCH_MMAP_RND_COMPAT_BITS_MAX - default ARCH_MMAP_RND_COMPAT_BITS_DEFAULT if ARCH_MMAP_RND_COMPAT_BITS_DEFAULT -- default ARCH_MMAP_RND_COMPAT_BITS_MIN -+ default ARCH_MMAP_RND_COMPAT_BITS_MAX - depends on HAVE_ARCH_MMAP_RND_COMPAT_BITS - help - This value can be used to select the number of bits to use to -diff --git a/arch/arm64/include/asm/elf.h b/arch/arm64/include/asm/elf.h -index 3f93f4eef953..575a608be260 100644 ---- a/arch/arm64/include/asm/elf.h -+++ b/arch/arm64/include/asm/elf.h -@@ -189,10 +189,10 @@ extern int arch_setup_additional_pages(struct linux_binprm *bprm, - /* 1GB of VA */ - #ifdef CONFIG_COMPAT - #define STACK_RND_MASK (test_thread_flag(TIF_32BIT) ? \ -- 0x7ff >> (PAGE_SHIFT - 12) : \ -- 0x3ffff >> (PAGE_SHIFT - 12)) -+ ((1UL << mmap_rnd_compat_bits) - 1) >> (PAGE_SHIFT - 12) : \ -+ ((1UL << mmap_rnd_bits) - 1) >> (PAGE_SHIFT - 12)) - #else --#define STACK_RND_MASK (0x3ffff >> (PAGE_SHIFT - 12)) -+#define STACK_RND_MASK (((1UL << mmap_rnd_bits) - 1) >> (PAGE_SHIFT - 12)) - #endif - - #ifdef __AARCH64EB__ -diff --git a/arch/x86/include/asm/elf.h b/arch/x86/include/asm/elf.h -index 1fb83d47711f..ecaafb34d79a 100644 ---- a/arch/x86/include/asm/elf.h -+++ b/arch/x86/include/asm/elf.h -@@ -318,8 +318,8 @@ extern unsigned long get_sigframe_size(void); - - #ifdef CONFIG_X86_32 - --#define __STACK_RND_MASK(is32bit) (0x7ff) --#define STACK_RND_MASK (0x7ff) -+#define __STACK_RND_MASK(is32bit) ((1UL << mmap_rnd_bits) - 1) -+#define STACK_RND_MASK ((1UL << mmap_rnd_bits) - 1) - - #define ARCH_DLINFO ARCH_DLINFO_IA32 - -@@ -328,7 +328,11 @@ extern unsigned long get_sigframe_size(void); - #else /* CONFIG_X86_32 */ - - /* 1GB for 64bit, 8MB for 32bit */ --#define __STACK_RND_MASK(is32bit) ((is32bit) ? 0x7ff : 0x3fffff) -+#ifdef CONFIG_COMPAT -+#define __STACK_RND_MASK(is32bit) ((is32bit) ? (1UL << mmap_rnd_compat_bits) - 1 : (1UL << mmap_rnd_bits) - 1) -+#else -+#define __STACK_RND_MASK(is32bit) ((1UL << mmap_rnd_bits) - 1) -+#endif - #define STACK_RND_MASK __STACK_RND_MASK(mmap_is_ia32()) - - #define ARCH_DLINFO \ -diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c -index b8441147eb5e..4eb46a1e86ce 100644 ---- a/arch/x86/kernel/process.c -+++ b/arch/x86/kernel/process.c -@@ -1001,9 +1001,9 @@ unsigned long arch_align_stack(unsigned long sp) - unsigned long arch_randomize_brk(struct mm_struct *mm) - { - if (mmap_is_ia32()) -- return randomize_page(mm->brk, SZ_32M); -+ return mm->brk + get_random_long() % SZ_32M + PAGE_SIZE; - -- return randomize_page(mm->brk, SZ_1G); -+ return mm->brk + get_random_long() % SZ_1G + PAGE_SIZE; - } - - /* -diff --git a/arch/x86/mm/fault.c b/arch/x86/mm/fault.c -index e6c469b323cc..5522c862ca76 100644 ---- a/arch/x86/mm/fault.c -+++ b/arch/x86/mm/fault.c -@@ -1199,6 +1199,217 @@ do_kern_addr_fault(struct pt_regs *regs, unsigned long hw_error_code, - } - NOKPROBE_SYMBOL(do_kern_addr_fault); - -+#ifdef CONFIG_OPENPAX_EMUTRAMP -+/* -+ * Determine if a fault is possibly caused by an emulatable stack or -+ * heap trampoline. We return false if trampoline emulation is not -+ * enabled. -+ */ -+static inline -+bool openpax_fault_is_trampoline(unsigned long error_code, -+ struct pt_regs *regs, -+ unsigned long address) -+{ -+ struct mm_struct *mm = current->mm; -+ unsigned long ip = regs->ip; -+ -+ if (!test_bit(PAXF_EMUTRAMP, &mm->pax_flags)) -+ return false; -+ -+ if (v8086_mode(regs)) -+ ip = ((regs->cs & 0xffff) << 4) + (ip & 0xffff); -+ -+ if (test_bit(PAXF_PAGEEXEC, &mm->pax_flags)) { -+ if ((__supported_pte_mask & _PAGE_NX) && (error_code & X86_PF_INSTR)) -+ return true; -+ if (!(error_code & (X86_PF_PROT | X86_PF_WRITE)) && ip == address) -+ return true; -+ return false; -+ } -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_fault_is_trampoline); -+ -+static inline -+bool openpax_emulate_trampoline_32(struct pt_regs *regs) -+{ -+ int err; -+ -+ /* libffi trampoline type 1, gcc trampoline type 2 */ -+ do { -+ unsigned char mov, jmp; -+ unsigned int addr1, addr2; -+ -+#ifdef CONFIG_X86_64 -+ if ((regs->ip + 9) >> 32) -+ break; -+#endif -+ -+ err = get_user(mov, (unsigned char __user *) regs->ip); -+ err |= get_user(addr1, (unsigned int __user *) (regs->ip + 1)); -+ err |= get_user(jmp, (unsigned char __user *) (regs->ip + 5)); -+ err |= get_user(addr2, (unsigned int __user *) (regs->ip + 6)); -+ -+ if (err) -+ break; -+ -+ if ((mov == 0xB8 || mov == 0xB9) && jmp == 0xE9) { -+ if (mov == 0xB8) -+ regs->ax = addr1; -+ else -+ regs->cx = addr1; -+ -+ regs->ip = (unsigned int)(regs->ip + addr2 + 10); -+ return true; -+ } -+ } while (0); -+ -+ /* older gcc trampoline type... */ -+ do { -+ unsigned char mov1, mov2; -+ unsigned short jmp; -+ unsigned int addr1, addr2; -+ -+#ifdef CONFIG_X86_64 -+ if ((regs->ip + 11) >> 32) -+ break; -+#endif -+ -+ err = get_user(mov1, (unsigned char __user *) regs->ip); -+ err |= get_user(addr1, (unsigned int __user *) (regs->ip + 1)); -+ err |= get_user(mov2, (unsigned char __user *) (regs->ip + 5)); -+ err |= get_user(addr2, (unsigned int __user *) (regs->ip + 6)); -+ err |= get_user(jmp, (unsigned short __user *) (regs->ip + 10)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xB9 && mov2 == 0xB8 && jmp == 0xE0FF) { -+ regs->cx = addr1; -+ regs->ax = addr2; -+ regs->ip = addr2; -+ return true; -+ } -+ } while (0); -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_emulate_trampoline_32); -+ -+#ifdef CONFIG_X86_64 -+static inline -+bool openpax_emulate_trampoline_64(struct pt_regs *regs) -+{ -+ int err; -+ -+ /* libffi trampoline type 1 */ -+ do { -+ unsigned short mov1, mov2, jmp1; -+ unsigned char stcclc, jmp2; -+ unsigned long addr1, addr2; -+ -+ err = get_user(mov1, (unsigned short __user *) regs->ip); -+ err |= get_user(addr1, (unsigned long __user *) (regs->ip + 2)); -+ err |= get_user(mov2, (unsigned short __user *) (regs->ip + 10)); -+ err |= get_user(addr2, (unsigned long __user *) (regs->ip + 12)); -+ err |= get_user(stcclc, (unsigned char __user *) (regs->ip + 20)); -+ err |= get_user(jmp1, (unsigned short __user *) (regs->ip + 21)); -+ err |= get_user(jmp2, (unsigned char __user *) (regs->ip + 23)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xBB49 && mov2 == 0xBA49 && (stcclc == 0xF8 || stcclc == 0xF9) && jmp1 == 0xFF49 && jmp2 == 0xE3) { -+ regs->r11 = addr1; -+ regs->r10 = addr2; -+ -+ if (stcclc == 0xF8) -+ regs->flags &= ~X86_EFLAGS_CF; -+ else -+ regs->flags |= X86_EFLAGS_CF; -+ -+ regs->ip = addr1; -+ return true; -+ } -+ } while (0); -+ -+ /* gcc trampoline type 1 */ -+ do { -+ unsigned short mov1, mov2, jmp1; -+ unsigned char jmp2; -+ unsigned int addr1; -+ unsigned long addr2; -+ -+ err = get_user(mov1, (unsigned short __user *) regs->ip); -+ err |= get_user(addr1, (unsigned int __user *) (regs->ip + 2)); -+ err |= get_user(mov2, (unsigned short __user *) (regs->ip + 6)); -+ err |= get_user(addr2, (unsigned long __user *) (regs->ip + 8)); -+ err |= get_user(jmp1, (unsigned short __user *) (regs->ip + 16)); -+ err |= get_user(jmp2, (unsigned char __user *) (regs->ip + 18)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xBB41 && mov2 == 0xBA49 && jmp1 == 0xFF49 && jmp2 == 0xE3) { -+ regs->r11 = addr1; -+ regs->r10 = addr2; -+ regs->ip = addr1; -+ return true; -+ } -+ } while (0); -+ -+ /* gcc trampoline type 2 */ -+ do { -+ unsigned short mov1, mov2, jmp1; -+ unsigned char jmp2; -+ unsigned long addr1, addr2; -+ -+ err = get_user(mov1, (unsigned short __user *) regs->ip); -+ err |= get_user(addr1, (unsigned long __user *) (regs->ip + 2)); -+ err |= get_user(mov2, (unsigned short __user *) (regs->ip + 10)); -+ err |= get_user(addr2, (unsigned long __user *) (regs->ip + 12)); -+ err |= get_user(jmp1, (unsigned short __user *) (regs->ip + 20)); -+ err |= get_user(jmp2, (unsigned char __user *) (regs->ip + 22)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xBB49 && mov2 == 0xBA49 && jmp1 == 0xFF49 && jmp2 == 0xE3) { -+ regs->r11 = addr1; -+ regs->r10 = addr2; -+ regs->ip = addr1; -+ return true; -+ } -+ } while (0); -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_emulate_trampoline_64); -+#endif -+ -+/* -+ * Emulate a trampoline. Returns false if emulation failed, meaning -+ * that the task should be killed. -+ */ -+static inline -+bool openpax_emulate_trampoline(struct pt_regs *regs) -+{ -+ if (v8086_mode(regs)) -+ return false; -+ -+ if (regs->cs == __USER32_CS || (regs->cs & SEGMENT_LDT)) -+ return openpax_emulate_trampoline_32(regs); -+#ifdef CONFIG_X86_64 -+ else -+ return openpax_emulate_trampoline_64(regs); -+#endif -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_emulate_trampoline); -+#endif -+ - /* - * Handle faults in the user portion of the address space. Nothing in here - * should check X86_PF_USER without a specific justification: for almost -@@ -1323,6 +1534,13 @@ void do_user_addr_fault(struct pt_regs *regs, - } - #endif - -+#ifdef CONFIG_OPENPAX_EMUTRAMP -+ if (openpax_fault_is_trampoline(error_code, regs, address)) { -+ if (openpax_emulate_trampoline(regs)) -+ return; -+ } -+#endif -+ - if (!(flags & FAULT_FLAG_USER)) - goto lock_mmap; - -diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c -index 777405719de8..dd8523f332a6 100644 ---- a/fs/binfmt_elf.c -+++ b/fs/binfmt_elf.c -@@ -47,6 +47,7 @@ - #include - #include - #include -+#include - #include - #include - -@@ -816,6 +817,72 @@ static int parse_elf_properties(struct file *f, const struct elf_phdr *phdr, - return ret == -ENOENT ? 0 : ret; - } - -+#ifdef CONFIG_OPENPAX -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+static int openpax_parse_xattr_flags(struct file * const file) -+{ -+ ssize_t xattr_size, i; -+ unsigned char xattr_value[sizeof("pemrs") - 1]; -+ -+ xattr_size = pax_getxattr(file, xattr_value, sizeof xattr_value); -+ if (xattr_size < 0 || xattr_size > sizeof xattr_value) -+ return -ENOENT; -+ -+ for (i = 0; i < xattr_size; i++) -+ switch (xattr_value[i]) { -+ default: -+ return -EINVAL; -+ -+#define parse_flag(option_disable, option_enable, flag) \ -+ case option_disable: \ -+ clear_bit(flag, ¤t->mm->pax_flags); \ -+ break; \ -+ case option_enable: \ -+ set_bit(flag, ¤t->mm->pax_flags); \ -+ break; -+ -+ parse_flag('p', 'P', PAXF_PAGEEXEC); -+ parse_flag('e', 'E', PAXF_EMUTRAMP); -+ parse_flag('m', 'M', PAXF_MPROTECT); -+ parse_flag('r', 'R', PAXF_RANDMMAP); -+ parse_flag('s', 'S', PAXF_SEGMEXEC); -+#undef parse_flag -+ } -+ -+ return 0; -+} -+#endif -+ -+static int openpax_set_flags(struct file * const file, const int snapshot_randomize_va_space) -+{ -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ int error; -+#endif -+ current->mm->pax_flags = 0; -+ -+ if (snapshot_randomize_va_space) { -+ set_bit(PAXF_RANDMMAP, ¤t->mm->pax_flags); -+ } -+ -+ if (!pax_softmode) { -+ set_bit(PAXF_PAGEEXEC, ¤t->mm->pax_flags); -+ set_bit(PAXF_MPROTECT, ¤t->mm->pax_flags); -+ } -+ -+#ifdef CONFIG_OPENPAX_EMUTRAMP_DEFAULT -+ set_bit(PAXF_EMUTRAMP, ¤t->mm->pax_flags); -+#endif -+ -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ error = openpax_parse_xattr_flags(file); -+ if (error != -ENOENT) -+ return error; -+#endif -+ -+ return 0; -+} -+#endif -+ - static int load_elf_binary(struct linux_binprm *bprm) - { - struct file *interpreter = NULL; /* to shut gcc up */ -@@ -1000,11 +1067,28 @@ static int load_elf_binary(struct linux_binprm *bprm) - /* Do this immediately, since STACK_TOP as used in setup_arg_pages - may depend on the personality. */ - SET_PERSONALITY2(*elf_ex, &arch_state); -+ -+ const int snapshot_randomize_va_space = READ_ONCE(randomize_va_space); -+ -+#ifdef CONFIG_OPENPAX -+ retval = openpax_set_flags(bprm->file, snapshot_randomize_va_space); -+ if (retval) -+ goto out_free_dentry; -+ -+ if (test_bit(PAXF_PAGEEXEC, ¤t->mm->pax_flags) || test_bit(PAXF_SEGMEXEC, ¤t->mm->pax_flags)) { -+ executable_stack = EXSTACK_DISABLE_X; -+ current->personality &= ~READ_IMPLIES_EXEC; -+ } else -+#endif -+ - if (elf_read_implies_exec(*elf_ex, executable_stack)) - current->personality |= READ_IMPLIES_EXEC; - -- const int snapshot_randomize_va_space = READ_ONCE(randomize_va_space); -- if (!(current->personality & ADDR_NO_RANDOMIZE) && snapshot_randomize_va_space) -+ if (!(current->personality & ADDR_NO_RANDOMIZE) && snapshot_randomize_va_space -+#ifdef CONFIG_OPENPAX -+ && test_bit(PAXF_RANDMMAP, ¤t->mm->pax_flags) -+#endif -+ ) - current->flags |= PF_RANDOMIZE; - - setup_new_exec(bprm); -diff --git a/fs/exec.c b/fs/exec.c -index 0c17e59e3767..dbda43319339 100644 ---- a/fs/exec.c -+++ b/fs/exec.c -@@ -68,6 +68,7 @@ - #include - #include - #include -+#include - - #include - #include -@@ -295,6 +296,10 @@ static int __bprm_mm_init(struct linux_binprm *bprm) - mm->stack_vm = mm->total_vm = 1; - mmap_write_unlock(mm); - bprm->p = vma->vm_end - sizeof(void *); -+ -+ if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space) -+ bprm->p ^= get_random_u32() & ~PAGE_MASK; -+ - return 0; - err: - ksm_exit(mm); -diff --git a/fs/proc/array.c b/fs/proc/array.c -index 34a47fb0c57f..cf22281d6fd9 100644 ---- a/fs/proc/array.c -+++ b/fs/proc/array.c -@@ -436,6 +436,18 @@ __weak void arch_proc_pid_thread_features(struct seq_file *m, - { - } - -+#ifdef CONFIG_OPENPAX -+static inline void task_pax(struct seq_file *m, struct mm_struct *mm) -+{ -+ seq_printf(m, "PaX:\t%c%c%c%c%c\n", -+ test_bit(PAXF_PAGEEXEC, &mm->pax_flags) ? 'P' : 'p', -+ test_bit(PAXF_EMUTRAMP, &mm->pax_flags) ? 'E' : 'e', -+ test_bit(PAXF_MPROTECT, &mm->pax_flags) ? 'M' : 'm', -+ test_bit(PAXF_RANDMMAP, &mm->pax_flags) ? 'R' : 'r', -+ test_bit(PAXF_SEGMEXEC, &mm->pax_flags) ? 'S' : 's'); -+} -+#endif -+ - int proc_pid_status(struct seq_file *m, struct pid_namespace *ns, - struct pid *pid, struct task_struct *task) - { -@@ -452,6 +464,9 @@ int proc_pid_status(struct seq_file *m, struct pid_namespace *ns, - task_core_dumping(m, task); - task_thp_status(m, mm); - task_untag_mask(m, mm); -+#ifdef CONFIG_OPENPAX -+ task_pax(m, mm); -+#endif - mmput(mm); - } - task_sig(m, task); -diff --git a/fs/xattr.c b/fs/xattr.c -index 7672ce5486c5..f3a2fd7c7a09 100644 ---- a/fs/xattr.c -+++ b/fs/xattr.c -@@ -424,6 +424,22 @@ __vfs_getxattr(struct dentry *dentry, struct inode *inode, const char *name, - } - EXPORT_SYMBOL(__vfs_getxattr); - -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ssize_t -+pax_getxattr(struct file *file, void *value, size_t size) -+{ -+ struct inode *inode = file->f_path.dentry->d_inode; -+ ssize_t error; -+ -+ error = inode_permission(file_mnt_idmap(file), inode, MAY_EXEC); -+ if (error) -+ return error; -+ -+ return __vfs_getxattr(file->f_path.dentry, inode, XATTR_NAME_USER_PAX_FLAGS, value, size); -+} -+EXPORT_SYMBOL(pax_getxattr); -+#endif -+ - ssize_t - vfs_getxattr(struct mnt_idmap *idmap, struct dentry *dentry, - const char *name, void *value, size_t size) -diff --git a/include/linux/init.h b/include/linux/init.h -index 58cef4c2e59a..3fe1ecc73c3a 100644 ---- a/include/linux/init.h -+++ b/include/linux/init.h -@@ -144,6 +144,9 @@ extern char __initdata boot_command_line[]; - extern char *saved_command_line; - extern unsigned int saved_command_line_len; - extern unsigned int reset_devices; -+#ifdef CONFIG_OPENPAX_SOFTMODE -+extern int pax_softmode; -+#endif - - /* used by init/main.c */ - void setup_arch(char **); -diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h -index af3a0256fa93..a6915152c7f0 100644 ---- a/include/linux/mm_types.h -+++ b/include/linux/mm_types.h -@@ -906,6 +906,9 @@ struct mm_struct { - mm_context_t context; - - unsigned long flags; /* Must use atomic bitops to access */ -+#ifdef CONFIG_OPENPAX -+ unsigned long pax_flags; -+#endif - - #ifdef CONFIG_AIO - spinlock_t ioctx_lock; -diff --git a/include/linux/mman.h b/include/linux/mman.h -index bcb201ab7a41..f01ff1102dbb 100644 ---- a/include/linux/mman.h -+++ b/include/linux/mman.h -@@ -188,11 +188,19 @@ static inline bool arch_memory_deny_write_exec_supported(void) - * - * d) mmap(PROT_READ | PROT_EXEC) - * mmap(PROT_READ | PROT_EXEC | PROT_BTI) -+ * -+ * If OpenPaX is enabled, it will be assumed that we want to deny PROT_WRITE | PROT_EXEC -+ * by default, unless the MPROTECT feature bit is disabled on a binary. - */ - static inline bool map_deny_write_exec(struct vm_area_struct *vma, unsigned long vm_flags) - { -- if (!test_bit(MMF_HAS_MDWE, ¤t->mm->flags)) -+ if ( -+#ifdef CONFIG_OPENPAX_MPROTECT -+ !test_bit(PAXF_MPROTECT, ¤t->mm->pax_flags) && -+#endif -+ !test_bit(MMF_HAS_MDWE, ¤t->mm->flags)) { - return false; -+ } - - if ((vm_flags & VM_EXEC) && (vm_flags & VM_WRITE)) - return true; -diff --git a/include/linux/sched/coredump.h b/include/linux/sched/coredump.h -index e62ff805cfc9..b5c31fed6311 100644 ---- a/include/linux/sched/coredump.h -+++ b/include/linux/sched/coredump.h -@@ -107,4 +107,12 @@ static inline unsigned long mmf_init_flags(unsigned long flags) - return flags & MMF_INIT_MASK; - } - -+#ifdef CONFIG_OPENPAX -+#define PAXF_PAGEEXEC 1 -+#define PAXF_EMUTRAMP 2 -+#define PAXF_MPROTECT 3 -+#define PAXF_RANDMMAP 4 -+#define PAXF_SEGMEXEC 5 -+#endif -+ - #endif /* _LINUX_SCHED_COREDUMP_H */ -diff --git a/include/linux/xattr.h b/include/linux/xattr.h -index d20051865800..e41b92f2b0dd 100644 ---- a/include/linux/xattr.h -+++ b/include/linux/xattr.h -@@ -21,6 +21,7 @@ - - struct inode; - struct dentry; -+struct file; - - static inline bool is_posix_acl_xattr(const char *name) - { -@@ -71,6 +72,9 @@ struct xattr { - size_t value_len; - }; - -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ssize_t pax_getxattr(struct file *, void *, size_t); -+#endif - ssize_t __vfs_getxattr(struct dentry *, struct inode *, const char *, void *, size_t); - ssize_t vfs_getxattr(struct mnt_idmap *, struct dentry *, const char *, - void *, size_t); -diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h -index 9463db2dfa9d..d4264c8df0fb 100644 ---- a/include/uapi/linux/xattr.h -+++ b/include/uapi/linux/xattr.h -@@ -81,5 +81,10 @@ - #define XATTR_POSIX_ACL_DEFAULT "posix_acl_default" - #define XATTR_NAME_POSIX_ACL_DEFAULT XATTR_SYSTEM_PREFIX XATTR_POSIX_ACL_DEFAULT - -+/* User namespace */ -+#define XATTR_PAX_PREFIX "pax." -+#define XATTR_PAX_FLAGS_SUFFIX "flags" -+#define XATTR_NAME_USER_PAX_FLAGS XATTR_USER_PREFIX XATTR_PAX_PREFIX XATTR_PAX_FLAGS_SUFFIX -+#define XATTR_NAME_PAX_FLAGS XATTR_PAX_PREFIX XATTR_PAX_FLAGS_SUFFIX - - #endif /* _UAPI_LINUX_XATTR_H */ -diff --git a/init/main.c b/init/main.c -index 206acdde51f5..49478af95719 100644 ---- a/init/main.c -+++ b/init/main.c -@@ -188,6 +188,17 @@ static int __init set_reset_devices(char *str) - - __setup("reset_devices", set_reset_devices); - -+#ifdef CONFIG_OPENPAX_SOFTMODE -+int pax_softmode; -+ -+static int __init setup_pax_softmode(char *str) -+{ -+ get_option(&str, &pax_softmode); -+ return 1; -+} -+__setup("pax_softmode=", setup_pax_softmode); -+#endif -+ - static const char *argv_init[MAX_INIT_ARGS+2] = { "init", NULL, }; - const char *envp_init[MAX_INIT_ENVS+2] = { "HOME=/", "TERM=linux", NULL, }; - static const char *panic_later, *panic_param; -diff --git a/kernel/sysctl.c b/kernel/sysctl.c -index e0b917328cf9..45651e1b00a2 100644 ---- a/kernel/sysctl.c -+++ b/kernel/sysctl.c -@@ -1615,6 +1615,18 @@ int proc_do_static_key(struct ctl_table *table, int write, - return ret; - } - -+#ifdef CONFIG_OPENPAX_SOFTMODE -+static struct ctl_table pax_table[] = { -+ { -+ .procname = "softmode", -+ .data = &pax_softmode, -+ .maxlen = sizeof(int), -+ .mode = 0600, -+ .proc_handler = proc_dointvec, -+ }, -+}; -+#endif -+ - static struct ctl_table kern_table[] = { - { - .procname = "panic", -@@ -2245,6 +2257,9 @@ int __init sysctl_init_bases(void) - { - register_sysctl_init("kernel", kern_table); - register_sysctl_init("vm", vm_table); -+#ifdef CONFIG_OPENPAX_SOFTMODE -+ register_sysctl_init("kernel/pax", pax_table); -+#endif - - return 0; - } -diff --git a/mm/mmap.c b/mm/mmap.c -index 83b4682ec85c..806d2aa015f5 100644 ---- a/mm/mmap.c -+++ b/mm/mmap.c -@@ -218,6 +218,13 @@ SYSCALL_DEFINE1(brk, unsigned long, brk) - - newbrk = PAGE_ALIGN(brk); - oldbrk = PAGE_ALIGN(mm->brk); -+ /* properly handle unaligned min_brk as an empty heap */ -+ if (min_brk & ~PAGE_MASK) { -+ if (brk == min_brk) -+ newbrk -= PAGE_SIZE; -+ if (mm->brk == min_brk) -+ oldbrk -= PAGE_SIZE; -+ } - if (oldbrk == newbrk) { - mm->brk = brk; - goto success; -diff --git a/mm/util.c b/mm/util.c -index fe723241b66f..8548383e9407 100644 ---- a/mm/util.c -+++ b/mm/util.c -@@ -389,9 +389,9 @@ unsigned long __weak arch_randomize_brk(struct mm_struct *mm) - { - /* Is the current task 32bit ? */ - if (!IS_ENABLED(CONFIG_64BIT) || is_compat_task()) -- return randomize_page(mm->brk, SZ_32M); -+ return mm->brk + get_random_long() % SZ_32M + PAGE_SIZE; - -- return randomize_page(mm->brk, SZ_1G); -+ return mm->brk + get_random_long() % SZ_1G + PAGE_SIZE; - } - - unsigned long arch_mmap_rnd(void) -diff --git a/security/Kconfig b/security/Kconfig -index 412e76f1575d..34e6641dcb56 100644 ---- a/security/Kconfig -+++ b/security/Kconfig -@@ -246,6 +246,7 @@ config LSM - If unsure, leave this as the default. - - source "security/Kconfig.hardening" -+source "security/Kconfig.openpax" - - endmenu - -diff --git a/security/Kconfig.openpax b/security/Kconfig.openpax -new file mode 100644 -index 000000000000..76ee145094d9 ---- /dev/null -+++ b/security/Kconfig.openpax -@@ -0,0 +1,89 @@ -+# -+# OpenPaX configuration -+# -+ -+menu "OpenPaX options" -+ -+config OPENPAX -+ bool "Enable OpenPaX features" -+ default y -+ help -+ This configuration setting enables OpenPaX features. -+ OpenPaX adds memory safety-related defenses to the kernel which -+ reduce the risks posed by exploitable memory safety bugs. -+ -+config OPENPAX_SOFTMODE -+ bool "Support PaX soft mode" -+ default y -+ help -+ Enabling this option will allow you to configure OpenPaX -+ features to run in soft mode. In this mode, OpenPaX features -+ will be disabled by default, only running on applications -+ which explicitly enable them. -+ -+ Soft mode can be enabled via the kernel.pax.softmode sysctl, -+ or the pax_softmode=1 kernel command-line option. -+ -+config OPENPAX_XATTR_PAX_FLAGS -+ bool "Use filesystem extended attributes to modify OpenPaX features" -+ depends on OPENPAX -+ default y -+ help -+ Enabling this option will allow you to control whether -+ OpenPaX features are enabled on a per-executable basis via -+ xattr attributes. -+ -+ For compatibility with the original PaX patch, the feature -+ flags are read from the user.pax.flags extended attribute. -+ -+ If you disable this feature, then all applications will run -+ with OpenPaX enabled by default. -+ -+config OPENPAX_MPROTECT -+ bool "Enforce W^X for memory mappings" -+ depends on OPENPAX -+ default y -+ help -+ Enabling this option prevents programs from making pages -+ executable when they are also writable. In addition, it -+ also denies transition of writable mappings to executable -+ mappings. -+ -+ This feature is known to break programs which depend on -+ just-in-time (JIT) compilation. It is advisable to enable -+ this feature system-wide, but mark programs which have -+ JIT compilation appropriately so the W^X enforcement is -+ disabled for them. -+ -+config OPENPAX_EMUTRAMP -+ bool "Emulate stack and heap trampolines" -+ depends on OPENPAX -+ default y -+ help -+ Enabling this option allows programs to depend on common -+ types of stack and heap trampolines (such as the ones -+ generated by GCC and libffi) to continue working despite -+ the stack and heap being non-executable memory. -+ -+ This option works by intercepting the page faults caused -+ by executing code in non-executable memory and emulating -+ the side effects that would have happened from executing -+ the trampoline. -+ -+ Most likely, you should say 'y' here. -+ -+config OPENPAX_EMUTRAMP_DEFAULT -+ bool "Enable trampoline emulation by default" -+ depends on OPENPAX_EMUTRAMP -+ default y -+ help -+ Enabling this option allows programs which require -+ trampolines to be emulated to continue working by default. -+ -+ Otherwise, the emulation flag must be enabled in a binary's -+ PaX marking, e.g. with paxmark -E . -+ -+ If you do not say 'y' here, you will have to manually mark -+ all programs which require trampoline emulation. -+ -+endmenu diff --git a/patches/openpax-6.11.2-20241009165538.patch b/patches/openpax-6.11.2-20241009165538.patch deleted file mode 100644 index 866784c6..00000000 --- a/patches/openpax-6.11.2-20241009165538.patch +++ /dev/null @@ -1,836 +0,0 @@ -diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt -index 09126bb8cc9f..91b264b7fc3b 100644 ---- a/Documentation/admin-guide/kernel-parameters.txt -+++ b/Documentation/admin-guide/kernel-parameters.txt -@@ -4353,6 +4353,9 @@ - the specified number of seconds. This is to be used if - your oopses keep scrolling off the screen. - -+ pax_softmode= -+ Enables OpenPaX soft mode if set to a non-zero value. -+ - pcbit= [HW,ISDN] - - pci=option[,option...] [PCI,EARLY] various PCI subsystem options. -diff --git a/arch/Kconfig b/arch/Kconfig -index 975dd22a2dbd..de69b8f5b5be 100644 ---- a/arch/Kconfig -+++ b/arch/Kconfig -@@ -1050,7 +1050,7 @@ config ARCH_MMAP_RND_BITS - int "Number of bits to use for ASLR of mmap base address" if EXPERT - range ARCH_MMAP_RND_BITS_MIN ARCH_MMAP_RND_BITS_MAX - default ARCH_MMAP_RND_BITS_DEFAULT if ARCH_MMAP_RND_BITS_DEFAULT -- default ARCH_MMAP_RND_BITS_MIN -+ default ARCH_MMAP_RND_BITS_MAX - depends on HAVE_ARCH_MMAP_RND_BITS - help - This value can be used to select the number of bits to use to -@@ -1084,7 +1084,7 @@ config ARCH_MMAP_RND_COMPAT_BITS - int "Number of bits to use for ASLR of mmap base address for compatible applications" if EXPERT - range ARCH_MMAP_RND_COMPAT_BITS_MIN ARCH_MMAP_RND_COMPAT_BITS_MAX - default ARCH_MMAP_RND_COMPAT_BITS_DEFAULT if ARCH_MMAP_RND_COMPAT_BITS_DEFAULT -- default ARCH_MMAP_RND_COMPAT_BITS_MIN -+ default ARCH_MMAP_RND_COMPAT_BITS_MAX - depends on HAVE_ARCH_MMAP_RND_COMPAT_BITS - help - This value can be used to select the number of bits to use to -diff --git a/arch/arm64/include/asm/elf.h b/arch/arm64/include/asm/elf.h -index 3f93f4eef953..575a608be260 100644 ---- a/arch/arm64/include/asm/elf.h -+++ b/arch/arm64/include/asm/elf.h -@@ -189,10 +189,10 @@ extern int arch_setup_additional_pages(struct linux_binprm *bprm, - /* 1GB of VA */ - #ifdef CONFIG_COMPAT - #define STACK_RND_MASK (test_thread_flag(TIF_32BIT) ? \ -- 0x7ff >> (PAGE_SHIFT - 12) : \ -- 0x3ffff >> (PAGE_SHIFT - 12)) -+ ((1UL << mmap_rnd_compat_bits) - 1) >> (PAGE_SHIFT - 12) : \ -+ ((1UL << mmap_rnd_bits) - 1) >> (PAGE_SHIFT - 12)) - #else --#define STACK_RND_MASK (0x3ffff >> (PAGE_SHIFT - 12)) -+#define STACK_RND_MASK (((1UL << mmap_rnd_bits) - 1) >> (PAGE_SHIFT - 12)) - #endif - - #ifdef __AARCH64EB__ -diff --git a/arch/x86/include/asm/elf.h b/arch/x86/include/asm/elf.h -index 1fb83d47711f..ecaafb34d79a 100644 ---- a/arch/x86/include/asm/elf.h -+++ b/arch/x86/include/asm/elf.h -@@ -318,8 +318,8 @@ extern unsigned long get_sigframe_size(void); - - #ifdef CONFIG_X86_32 - --#define __STACK_RND_MASK(is32bit) (0x7ff) --#define STACK_RND_MASK (0x7ff) -+#define __STACK_RND_MASK(is32bit) ((1UL << mmap_rnd_bits) - 1) -+#define STACK_RND_MASK ((1UL << mmap_rnd_bits) - 1) - - #define ARCH_DLINFO ARCH_DLINFO_IA32 - -@@ -328,7 +328,11 @@ extern unsigned long get_sigframe_size(void); - #else /* CONFIG_X86_32 */ - - /* 1GB for 64bit, 8MB for 32bit */ --#define __STACK_RND_MASK(is32bit) ((is32bit) ? 0x7ff : 0x3fffff) -+#ifdef CONFIG_COMPAT -+#define __STACK_RND_MASK(is32bit) ((is32bit) ? (1UL << mmap_rnd_compat_bits) - 1 : (1UL << mmap_rnd_bits) - 1) -+#else -+#define __STACK_RND_MASK(is32bit) ((1UL << mmap_rnd_bits) - 1) -+#endif - #define STACK_RND_MASK __STACK_RND_MASK(mmap_is_ia32()) - - #define ARCH_DLINFO \ -diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c -index f63f8fd00a91..73cca55bb253 100644 ---- a/arch/x86/kernel/process.c -+++ b/arch/x86/kernel/process.c -@@ -1008,9 +1008,9 @@ unsigned long arch_align_stack(unsigned long sp) - unsigned long arch_randomize_brk(struct mm_struct *mm) - { - if (mmap_is_ia32()) -- return randomize_page(mm->brk, SZ_32M); -+ return mm->brk + get_random_long() % SZ_32M + PAGE_SIZE; - -- return randomize_page(mm->brk, SZ_1G); -+ return mm->brk + get_random_long() % SZ_1G + PAGE_SIZE; - } - - /* -diff --git a/arch/x86/mm/fault.c b/arch/x86/mm/fault.c -index e6c469b323cc..5522c862ca76 100644 ---- a/arch/x86/mm/fault.c -+++ b/arch/x86/mm/fault.c -@@ -1199,6 +1199,217 @@ do_kern_addr_fault(struct pt_regs *regs, unsigned long hw_error_code, - } - NOKPROBE_SYMBOL(do_kern_addr_fault); - -+#ifdef CONFIG_OPENPAX_EMUTRAMP -+/* -+ * Determine if a fault is possibly caused by an emulatable stack or -+ * heap trampoline. We return false if trampoline emulation is not -+ * enabled. -+ */ -+static inline -+bool openpax_fault_is_trampoline(unsigned long error_code, -+ struct pt_regs *regs, -+ unsigned long address) -+{ -+ struct mm_struct *mm = current->mm; -+ unsigned long ip = regs->ip; -+ -+ if (!test_bit(PAXF_EMUTRAMP, &mm->pax_flags)) -+ return false; -+ -+ if (v8086_mode(regs)) -+ ip = ((regs->cs & 0xffff) << 4) + (ip & 0xffff); -+ -+ if (test_bit(PAXF_PAGEEXEC, &mm->pax_flags)) { -+ if ((__supported_pte_mask & _PAGE_NX) && (error_code & X86_PF_INSTR)) -+ return true; -+ if (!(error_code & (X86_PF_PROT | X86_PF_WRITE)) && ip == address) -+ return true; -+ return false; -+ } -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_fault_is_trampoline); -+ -+static inline -+bool openpax_emulate_trampoline_32(struct pt_regs *regs) -+{ -+ int err; -+ -+ /* libffi trampoline type 1, gcc trampoline type 2 */ -+ do { -+ unsigned char mov, jmp; -+ unsigned int addr1, addr2; -+ -+#ifdef CONFIG_X86_64 -+ if ((regs->ip + 9) >> 32) -+ break; -+#endif -+ -+ err = get_user(mov, (unsigned char __user *) regs->ip); -+ err |= get_user(addr1, (unsigned int __user *) (regs->ip + 1)); -+ err |= get_user(jmp, (unsigned char __user *) (regs->ip + 5)); -+ err |= get_user(addr2, (unsigned int __user *) (regs->ip + 6)); -+ -+ if (err) -+ break; -+ -+ if ((mov == 0xB8 || mov == 0xB9) && jmp == 0xE9) { -+ if (mov == 0xB8) -+ regs->ax = addr1; -+ else -+ regs->cx = addr1; -+ -+ regs->ip = (unsigned int)(regs->ip + addr2 + 10); -+ return true; -+ } -+ } while (0); -+ -+ /* older gcc trampoline type... */ -+ do { -+ unsigned char mov1, mov2; -+ unsigned short jmp; -+ unsigned int addr1, addr2; -+ -+#ifdef CONFIG_X86_64 -+ if ((regs->ip + 11) >> 32) -+ break; -+#endif -+ -+ err = get_user(mov1, (unsigned char __user *) regs->ip); -+ err |= get_user(addr1, (unsigned int __user *) (regs->ip + 1)); -+ err |= get_user(mov2, (unsigned char __user *) (regs->ip + 5)); -+ err |= get_user(addr2, (unsigned int __user *) (regs->ip + 6)); -+ err |= get_user(jmp, (unsigned short __user *) (regs->ip + 10)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xB9 && mov2 == 0xB8 && jmp == 0xE0FF) { -+ regs->cx = addr1; -+ regs->ax = addr2; -+ regs->ip = addr2; -+ return true; -+ } -+ } while (0); -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_emulate_trampoline_32); -+ -+#ifdef CONFIG_X86_64 -+static inline -+bool openpax_emulate_trampoline_64(struct pt_regs *regs) -+{ -+ int err; -+ -+ /* libffi trampoline type 1 */ -+ do { -+ unsigned short mov1, mov2, jmp1; -+ unsigned char stcclc, jmp2; -+ unsigned long addr1, addr2; -+ -+ err = get_user(mov1, (unsigned short __user *) regs->ip); -+ err |= get_user(addr1, (unsigned long __user *) (regs->ip + 2)); -+ err |= get_user(mov2, (unsigned short __user *) (regs->ip + 10)); -+ err |= get_user(addr2, (unsigned long __user *) (regs->ip + 12)); -+ err |= get_user(stcclc, (unsigned char __user *) (regs->ip + 20)); -+ err |= get_user(jmp1, (unsigned short __user *) (regs->ip + 21)); -+ err |= get_user(jmp2, (unsigned char __user *) (regs->ip + 23)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xBB49 && mov2 == 0xBA49 && (stcclc == 0xF8 || stcclc == 0xF9) && jmp1 == 0xFF49 && jmp2 == 0xE3) { -+ regs->r11 = addr1; -+ regs->r10 = addr2; -+ -+ if (stcclc == 0xF8) -+ regs->flags &= ~X86_EFLAGS_CF; -+ else -+ regs->flags |= X86_EFLAGS_CF; -+ -+ regs->ip = addr1; -+ return true; -+ } -+ } while (0); -+ -+ /* gcc trampoline type 1 */ -+ do { -+ unsigned short mov1, mov2, jmp1; -+ unsigned char jmp2; -+ unsigned int addr1; -+ unsigned long addr2; -+ -+ err = get_user(mov1, (unsigned short __user *) regs->ip); -+ err |= get_user(addr1, (unsigned int __user *) (regs->ip + 2)); -+ err |= get_user(mov2, (unsigned short __user *) (regs->ip + 6)); -+ err |= get_user(addr2, (unsigned long __user *) (regs->ip + 8)); -+ err |= get_user(jmp1, (unsigned short __user *) (regs->ip + 16)); -+ err |= get_user(jmp2, (unsigned char __user *) (regs->ip + 18)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xBB41 && mov2 == 0xBA49 && jmp1 == 0xFF49 && jmp2 == 0xE3) { -+ regs->r11 = addr1; -+ regs->r10 = addr2; -+ regs->ip = addr1; -+ return true; -+ } -+ } while (0); -+ -+ /* gcc trampoline type 2 */ -+ do { -+ unsigned short mov1, mov2, jmp1; -+ unsigned char jmp2; -+ unsigned long addr1, addr2; -+ -+ err = get_user(mov1, (unsigned short __user *) regs->ip); -+ err |= get_user(addr1, (unsigned long __user *) (regs->ip + 2)); -+ err |= get_user(mov2, (unsigned short __user *) (regs->ip + 10)); -+ err |= get_user(addr2, (unsigned long __user *) (regs->ip + 12)); -+ err |= get_user(jmp1, (unsigned short __user *) (regs->ip + 20)); -+ err |= get_user(jmp2, (unsigned char __user *) (regs->ip + 22)); -+ -+ if (err) -+ break; -+ -+ if (mov1 == 0xBB49 && mov2 == 0xBA49 && jmp1 == 0xFF49 && jmp2 == 0xE3) { -+ regs->r11 = addr1; -+ regs->r10 = addr2; -+ regs->ip = addr1; -+ return true; -+ } -+ } while (0); -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_emulate_trampoline_64); -+#endif -+ -+/* -+ * Emulate a trampoline. Returns false if emulation failed, meaning -+ * that the task should be killed. -+ */ -+static inline -+bool openpax_emulate_trampoline(struct pt_regs *regs) -+{ -+ if (v8086_mode(regs)) -+ return false; -+ -+ if (regs->cs == __USER32_CS || (regs->cs & SEGMENT_LDT)) -+ return openpax_emulate_trampoline_32(regs); -+#ifdef CONFIG_X86_64 -+ else -+ return openpax_emulate_trampoline_64(regs); -+#endif -+ -+ return false; -+} -+NOKPROBE_SYMBOL(openpax_emulate_trampoline); -+#endif -+ - /* - * Handle faults in the user portion of the address space. Nothing in here - * should check X86_PF_USER without a specific justification: for almost -@@ -1323,6 +1534,13 @@ void do_user_addr_fault(struct pt_regs *regs, - } - #endif - -+#ifdef CONFIG_OPENPAX_EMUTRAMP -+ if (openpax_fault_is_trampoline(error_code, regs, address)) { -+ if (openpax_emulate_trampoline(regs)) -+ return; -+ } -+#endif -+ - if (!(flags & FAULT_FLAG_USER)) - goto lock_mmap; - -diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c -index 19fa49cd9907..f1337ae91755 100644 ---- a/fs/binfmt_elf.c -+++ b/fs/binfmt_elf.c -@@ -47,6 +47,7 @@ - #include - #include - #include -+#include - #include - #include - -@@ -816,6 +817,72 @@ static int parse_elf_properties(struct file *f, const struct elf_phdr *phdr, - return ret == -ENOENT ? 0 : ret; - } - -+#ifdef CONFIG_OPENPAX -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+static int openpax_parse_xattr_flags(struct file * const file) -+{ -+ ssize_t xattr_size, i; -+ unsigned char xattr_value[sizeof("pemrs") - 1]; -+ -+ xattr_size = pax_getxattr(file, xattr_value, sizeof xattr_value); -+ if (xattr_size < 0 || xattr_size > sizeof xattr_value) -+ return -ENOENT; -+ -+ for (i = 0; i < xattr_size; i++) -+ switch (xattr_value[i]) { -+ default: -+ return -EINVAL; -+ -+#define parse_flag(option_disable, option_enable, flag) \ -+ case option_disable: \ -+ clear_bit(flag, ¤t->mm->pax_flags); \ -+ break; \ -+ case option_enable: \ -+ set_bit(flag, ¤t->mm->pax_flags); \ -+ break; -+ -+ parse_flag('p', 'P', PAXF_PAGEEXEC); -+ parse_flag('e', 'E', PAXF_EMUTRAMP); -+ parse_flag('m', 'M', PAXF_MPROTECT); -+ parse_flag('r', 'R', PAXF_RANDMMAP); -+ parse_flag('s', 'S', PAXF_SEGMEXEC); -+#undef parse_flag -+ } -+ -+ return 0; -+} -+#endif -+ -+static int openpax_set_flags(struct file * const file, const int snapshot_randomize_va_space) -+{ -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ int error; -+#endif -+ current->mm->pax_flags = 0; -+ -+ if (snapshot_randomize_va_space) { -+ set_bit(PAXF_RANDMMAP, ¤t->mm->pax_flags); -+ } -+ -+ if (!pax_softmode) { -+ set_bit(PAXF_PAGEEXEC, ¤t->mm->pax_flags); -+ set_bit(PAXF_MPROTECT, ¤t->mm->pax_flags); -+ } -+ -+#ifdef CONFIG_OPENPAX_EMUTRAMP_DEFAULT -+ set_bit(PAXF_EMUTRAMP, ¤t->mm->pax_flags); -+#endif -+ -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ error = openpax_parse_xattr_flags(file); -+ if (error != -ENOENT) -+ return error; -+#endif -+ -+ return 0; -+} -+#endif -+ - static int load_elf_binary(struct linux_binprm *bprm) - { - struct file *interpreter = NULL; /* to shut gcc up */ -@@ -1000,11 +1067,28 @@ static int load_elf_binary(struct linux_binprm *bprm) - /* Do this immediately, since STACK_TOP as used in setup_arg_pages - may depend on the personality. */ - SET_PERSONALITY2(*elf_ex, &arch_state); -+ -+ const int snapshot_randomize_va_space = READ_ONCE(randomize_va_space); -+ -+#ifdef CONFIG_OPENPAX -+ retval = openpax_set_flags(bprm->file, snapshot_randomize_va_space); -+ if (retval) -+ goto out_free_dentry; -+ -+ if (test_bit(PAXF_PAGEEXEC, ¤t->mm->pax_flags) || test_bit(PAXF_SEGMEXEC, ¤t->mm->pax_flags)) { -+ executable_stack = EXSTACK_DISABLE_X; -+ current->personality &= ~READ_IMPLIES_EXEC; -+ } else -+#endif -+ - if (elf_read_implies_exec(*elf_ex, executable_stack)) - current->personality |= READ_IMPLIES_EXEC; - -- const int snapshot_randomize_va_space = READ_ONCE(randomize_va_space); -- if (!(current->personality & ADDR_NO_RANDOMIZE) && snapshot_randomize_va_space) -+ if (!(current->personality & ADDR_NO_RANDOMIZE) && snapshot_randomize_va_space -+#ifdef CONFIG_OPENPAX -+ && test_bit(PAXF_RANDMMAP, ¤t->mm->pax_flags) -+#endif -+ ) - current->flags |= PF_RANDOMIZE; - - setup_new_exec(bprm); -diff --git a/fs/exec.c b/fs/exec.c -index 50e76cc633c4..4c31766d32da 100644 ---- a/fs/exec.c -+++ b/fs/exec.c -@@ -68,6 +68,7 @@ - #include - #include - #include -+#include - - #include - #include -@@ -295,6 +296,10 @@ static int __bprm_mm_init(struct linux_binprm *bprm) - mm->stack_vm = mm->total_vm = 1; - mmap_write_unlock(mm); - bprm->p = vma->vm_end - sizeof(void *); -+ -+ if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space) -+ bprm->p ^= get_random_u32() & ~PAGE_MASK; -+ - return 0; - err: - ksm_exit(mm); -diff --git a/fs/proc/array.c b/fs/proc/array.c -index 34a47fb0c57f..cf22281d6fd9 100644 ---- a/fs/proc/array.c -+++ b/fs/proc/array.c -@@ -436,6 +436,18 @@ __weak void arch_proc_pid_thread_features(struct seq_file *m, - { - } - -+#ifdef CONFIG_OPENPAX -+static inline void task_pax(struct seq_file *m, struct mm_struct *mm) -+{ -+ seq_printf(m, "PaX:\t%c%c%c%c%c\n", -+ test_bit(PAXF_PAGEEXEC, &mm->pax_flags) ? 'P' : 'p', -+ test_bit(PAXF_EMUTRAMP, &mm->pax_flags) ? 'E' : 'e', -+ test_bit(PAXF_MPROTECT, &mm->pax_flags) ? 'M' : 'm', -+ test_bit(PAXF_RANDMMAP, &mm->pax_flags) ? 'R' : 'r', -+ test_bit(PAXF_SEGMEXEC, &mm->pax_flags) ? 'S' : 's'); -+} -+#endif -+ - int proc_pid_status(struct seq_file *m, struct pid_namespace *ns, - struct pid *pid, struct task_struct *task) - { -@@ -452,6 +464,9 @@ int proc_pid_status(struct seq_file *m, struct pid_namespace *ns, - task_core_dumping(m, task); - task_thp_status(m, mm); - task_untag_mask(m, mm); -+#ifdef CONFIG_OPENPAX -+ task_pax(m, mm); -+#endif - mmput(mm); - } - task_sig(m, task); -diff --git a/fs/xattr.c b/fs/xattr.c -index 7672ce5486c5..f3a2fd7c7a09 100644 ---- a/fs/xattr.c -+++ b/fs/xattr.c -@@ -424,6 +424,22 @@ __vfs_getxattr(struct dentry *dentry, struct inode *inode, const char *name, - } - EXPORT_SYMBOL(__vfs_getxattr); - -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ssize_t -+pax_getxattr(struct file *file, void *value, size_t size) -+{ -+ struct inode *inode = file->f_path.dentry->d_inode; -+ ssize_t error; -+ -+ error = inode_permission(file_mnt_idmap(file), inode, MAY_EXEC); -+ if (error) -+ return error; -+ -+ return __vfs_getxattr(file->f_path.dentry, inode, XATTR_NAME_USER_PAX_FLAGS, value, size); -+} -+EXPORT_SYMBOL(pax_getxattr); -+#endif -+ - ssize_t - vfs_getxattr(struct mnt_idmap *idmap, struct dentry *dentry, - const char *name, void *value, size_t size) -diff --git a/include/linux/init.h b/include/linux/init.h -index ee1309473bc6..97e901993802 100644 ---- a/include/linux/init.h -+++ b/include/linux/init.h -@@ -144,6 +144,9 @@ extern char __initdata boot_command_line[]; - extern char *saved_command_line; - extern unsigned int saved_command_line_len; - extern unsigned int reset_devices; -+#ifdef CONFIG_OPENPAX_SOFTMODE -+extern int pax_softmode; -+#endif - - /* used by init/main.c */ - void setup_arch(char **); -diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h -index 485424979254..7868b17e688e 100644 ---- a/include/linux/mm_types.h -+++ b/include/linux/mm_types.h -@@ -922,6 +922,9 @@ struct mm_struct { - mm_context_t context; - - unsigned long flags; /* Must use atomic bitops to access */ -+#ifdef CONFIG_OPENPAX -+ unsigned long pax_flags; -+#endif - - #ifdef CONFIG_AIO - spinlock_t ioctx_lock; -diff --git a/include/linux/mman.h b/include/linux/mman.h -index bcb201ab7a41..f01ff1102dbb 100644 ---- a/include/linux/mman.h -+++ b/include/linux/mman.h -@@ -188,11 +188,19 @@ static inline bool arch_memory_deny_write_exec_supported(void) - * - * d) mmap(PROT_READ | PROT_EXEC) - * mmap(PROT_READ | PROT_EXEC | PROT_BTI) -+ * -+ * If OpenPaX is enabled, it will be assumed that we want to deny PROT_WRITE | PROT_EXEC -+ * by default, unless the MPROTECT feature bit is disabled on a binary. - */ - static inline bool map_deny_write_exec(struct vm_area_struct *vma, unsigned long vm_flags) - { -- if (!test_bit(MMF_HAS_MDWE, ¤t->mm->flags)) -+ if ( -+#ifdef CONFIG_OPENPAX_MPROTECT -+ !test_bit(PAXF_MPROTECT, ¤t->mm->pax_flags) && -+#endif -+ !test_bit(MMF_HAS_MDWE, ¤t->mm->flags)) { - return false; -+ } - - if ((vm_flags & VM_EXEC) && (vm_flags & VM_WRITE)) - return true; -diff --git a/include/linux/sched/coredump.h b/include/linux/sched/coredump.h -index e62ff805cfc9..b5c31fed6311 100644 ---- a/include/linux/sched/coredump.h -+++ b/include/linux/sched/coredump.h -@@ -107,4 +107,12 @@ static inline unsigned long mmf_init_flags(unsigned long flags) - return flags & MMF_INIT_MASK; - } - -+#ifdef CONFIG_OPENPAX -+#define PAXF_PAGEEXEC 1 -+#define PAXF_EMUTRAMP 2 -+#define PAXF_MPROTECT 3 -+#define PAXF_RANDMMAP 4 -+#define PAXF_SEGMEXEC 5 -+#endif -+ - #endif /* _LINUX_SCHED_COREDUMP_H */ -diff --git a/include/linux/xattr.h b/include/linux/xattr.h -index d20051865800..e41b92f2b0dd 100644 ---- a/include/linux/xattr.h -+++ b/include/linux/xattr.h -@@ -21,6 +21,7 @@ - - struct inode; - struct dentry; -+struct file; - - static inline bool is_posix_acl_xattr(const char *name) - { -@@ -71,6 +72,9 @@ struct xattr { - size_t value_len; - }; - -+#ifdef CONFIG_OPENPAX_XATTR_PAX_FLAGS -+ssize_t pax_getxattr(struct file *, void *, size_t); -+#endif - ssize_t __vfs_getxattr(struct dentry *, struct inode *, const char *, void *, size_t); - ssize_t vfs_getxattr(struct mnt_idmap *, struct dentry *, const char *, - void *, size_t); -diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h -index 9463db2dfa9d..d4264c8df0fb 100644 ---- a/include/uapi/linux/xattr.h -+++ b/include/uapi/linux/xattr.h -@@ -81,5 +81,10 @@ - #define XATTR_POSIX_ACL_DEFAULT "posix_acl_default" - #define XATTR_NAME_POSIX_ACL_DEFAULT XATTR_SYSTEM_PREFIX XATTR_POSIX_ACL_DEFAULT - -+/* User namespace */ -+#define XATTR_PAX_PREFIX "pax." -+#define XATTR_PAX_FLAGS_SUFFIX "flags" -+#define XATTR_NAME_USER_PAX_FLAGS XATTR_USER_PREFIX XATTR_PAX_PREFIX XATTR_PAX_FLAGS_SUFFIX -+#define XATTR_NAME_PAX_FLAGS XATTR_PAX_PREFIX XATTR_PAX_FLAGS_SUFFIX - - #endif /* _UAPI_LINUX_XATTR_H */ -diff --git a/init/main.c b/init/main.c -index 206acdde51f5..49478af95719 100644 ---- a/init/main.c -+++ b/init/main.c -@@ -188,6 +188,17 @@ static int __init set_reset_devices(char *str) - - __setup("reset_devices", set_reset_devices); - -+#ifdef CONFIG_OPENPAX_SOFTMODE -+int pax_softmode; -+ -+static int __init setup_pax_softmode(char *str) -+{ -+ get_option(&str, &pax_softmode); -+ return 1; -+} -+__setup("pax_softmode=", setup_pax_softmode); -+#endif -+ - static const char *argv_init[MAX_INIT_ARGS+2] = { "init", NULL, }; - const char *envp_init[MAX_INIT_ENVS+2] = { "HOME=/", "TERM=linux", NULL, }; - static const char *panic_later, *panic_param; -diff --git a/kernel/sysctl.c b/kernel/sysctl.c -index 79e6cb1d5c48..e28466b47720 100644 ---- a/kernel/sysctl.c -+++ b/kernel/sysctl.c -@@ -1610,6 +1610,18 @@ int proc_do_static_key(const struct ctl_table *table, int write, - return ret; - } - -+#ifdef CONFIG_OPENPAX_SOFTMODE -+static struct ctl_table pax_table[] = { -+ { -+ .procname = "softmode", -+ .data = &pax_softmode, -+ .maxlen = sizeof(int), -+ .mode = 0600, -+ .proc_handler = proc_dointvec, -+ }, -+}; -+#endif -+ - static struct ctl_table kern_table[] = { - { - .procname = "panic", -@@ -2240,6 +2252,9 @@ int __init sysctl_init_bases(void) - { - register_sysctl_init("kernel", kern_table); - register_sysctl_init("vm", vm_table); -+#ifdef CONFIG_OPENPAX_SOFTMODE -+ register_sysctl_init("kernel/pax", pax_table); -+#endif - - return 0; - } -diff --git a/mm/mmap.c b/mm/mmap.c -index 18fddcce03b8..11a713e224c5 100644 ---- a/mm/mmap.c -+++ b/mm/mmap.c -@@ -259,6 +259,13 @@ SYSCALL_DEFINE1(brk, unsigned long, brk) - - newbrk = PAGE_ALIGN(brk); - oldbrk = PAGE_ALIGN(mm->brk); -+ /* properly handle unaligned min_brk as an empty heap */ -+ if (min_brk & ~PAGE_MASK) { -+ if (brk == min_brk) -+ newbrk -= PAGE_SIZE; -+ if (mm->brk == min_brk) -+ oldbrk -= PAGE_SIZE; -+ } - if (oldbrk == newbrk) { - mm->brk = brk; - goto success; -diff --git a/mm/util.c b/mm/util.c -index baca6cafc9f1..5319dfc5f137 100644 ---- a/mm/util.c -+++ b/mm/util.c -@@ -401,9 +401,9 @@ unsigned long __weak arch_randomize_brk(struct mm_struct *mm) - { - /* Is the current task 32bit ? */ - if (!IS_ENABLED(CONFIG_64BIT) || is_compat_task()) -- return randomize_page(mm->brk, SZ_32M); -+ return mm->brk + get_random_long() % SZ_32M + PAGE_SIZE; - -- return randomize_page(mm->brk, SZ_1G); -+ return mm->brk + get_random_long() % SZ_1G + PAGE_SIZE; - } - - unsigned long arch_mmap_rnd(void) -diff --git a/security/Kconfig b/security/Kconfig -index 412e76f1575d..34e6641dcb56 100644 ---- a/security/Kconfig -+++ b/security/Kconfig -@@ -246,6 +246,7 @@ config LSM - If unsure, leave this as the default. - - source "security/Kconfig.hardening" -+source "security/Kconfig.openpax" - - endmenu - -diff --git a/security/Kconfig.openpax b/security/Kconfig.openpax -new file mode 100644 -index 000000000000..76ee145094d9 ---- /dev/null -+++ b/security/Kconfig.openpax -@@ -0,0 +1,89 @@ -+# -+# OpenPaX configuration -+# -+ -+menu "OpenPaX options" -+ -+config OPENPAX -+ bool "Enable OpenPaX features" -+ default y -+ help -+ This configuration setting enables OpenPaX features. -+ OpenPaX adds memory safety-related defenses to the kernel which -+ reduce the risks posed by exploitable memory safety bugs. -+ -+config OPENPAX_SOFTMODE -+ bool "Support PaX soft mode" -+ default y -+ help -+ Enabling this option will allow you to configure OpenPaX -+ features to run in soft mode. In this mode, OpenPaX features -+ will be disabled by default, only running on applications -+ which explicitly enable them. -+ -+ Soft mode can be enabled via the kernel.pax.softmode sysctl, -+ or the pax_softmode=1 kernel command-line option. -+ -+config OPENPAX_XATTR_PAX_FLAGS -+ bool "Use filesystem extended attributes to modify OpenPaX features" -+ depends on OPENPAX -+ default y -+ help -+ Enabling this option will allow you to control whether -+ OpenPaX features are enabled on a per-executable basis via -+ xattr attributes. -+ -+ For compatibility with the original PaX patch, the feature -+ flags are read from the user.pax.flags extended attribute. -+ -+ If you disable this feature, then all applications will run -+ with OpenPaX enabled by default. -+ -+config OPENPAX_MPROTECT -+ bool "Enforce W^X for memory mappings" -+ depends on OPENPAX -+ default y -+ help -+ Enabling this option prevents programs from making pages -+ executable when they are also writable. In addition, it -+ also denies transition of writable mappings to executable -+ mappings. -+ -+ This feature is known to break programs which depend on -+ just-in-time (JIT) compilation. It is advisable to enable -+ this feature system-wide, but mark programs which have -+ JIT compilation appropriately so the W^X enforcement is -+ disabled for them. -+ -+config OPENPAX_EMUTRAMP -+ bool "Emulate stack and heap trampolines" -+ depends on OPENPAX -+ default y -+ help -+ Enabling this option allows programs to depend on common -+ types of stack and heap trampolines (such as the ones -+ generated by GCC and libffi) to continue working despite -+ the stack and heap being non-executable memory. -+ -+ This option works by intercepting the page faults caused -+ by executing code in non-executable memory and emulating -+ the side effects that would have happened from executing -+ the trampoline. -+ -+ Most likely, you should say 'y' here. -+ -+config OPENPAX_EMUTRAMP_DEFAULT -+ bool "Enable trampoline emulation by default" -+ depends on OPENPAX_EMUTRAMP -+ default y -+ help -+ Enabling this option allows programs which require -+ trampolines to be emulated to continue working by default. -+ -+ Otherwise, the emulation flag must be enabled in a binary's -+ PaX marking, e.g. with paxmark -E . -+ -+ If you do not say 'y' here, you will have to manually mark -+ all programs which require trampoline emulation. -+ -+endmenu diff --git a/patches/pv-iommu-6.18/0001-iommu-xen-Add-Xen-PV-IOMMU-driver.patch b/patches/pv-iommu-6.18/0001-iommu-xen-Add-Xen-PV-IOMMU-driver.patch deleted file mode 100644 index 10529830..00000000 --- a/patches/pv-iommu-6.18/0001-iommu-xen-Add-Xen-PV-IOMMU-driver.patch +++ /dev/null @@ -1,1370 +0,0 @@ -From 5bfc4264ba8f48808ec4f05763548e320374e07b Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Wed, 2 Sep 2026 00:38:42 -0700 -Subject: [PATCH 01/11] iommu/xen: Add Xen PV-IOMMU driver - -Port of Teddy Astie's RFC v3 driver for the Xen PV-IOMMU interface. Under -Xen the IOMMU belongs to the hypervisor, so a guest cannot use anything -that needs one -- VFIO, or DMA protection of its own. The PV-IOMMU -hypercall lets it manage IOMMU contexts through Xen instead, and this -driver presents that as a normal iommu_ops. - - https://patchwork.kernel.org/project/xen-devel/list/?series=906092 - -Changes needed against 6.18: - - - pgsize_bitmap moved from struct iommu_ops to struct iommu_domain, so - the mask the hypervisor reports is now stashed and applied to each - domain as it is allocated, including the identity domain. - - iommu_alloc_page()/iommu_free_page() are gone; the page table code uses - iommu_alloc_pages_sz()/iommu_free_pages(). - -CONFIG_XEN_IOMMU still depends on XEN_DOM0, matching the interface as -posted: the hypercall names devices by machine BDF, which a guest does not -see. ---- - arch/x86/include/asm/xen/hypercall.h | 6 + - drivers/iommu/Kconfig | 10 + - drivers/iommu/Makefile | 2 + - drivers/iommu/io-pgtable-xen.c | 370 ++++++++++++++++++++++ - drivers/iommu/io-pgtable.c | 3 + - drivers/iommu/xen-iommu.c | 446 +++++++++++++++++++++++++++ - include/linux/io-pgtable.h | 2 + - include/xen/interface/memory.h | 33 ++ - include/xen/interface/pv-iommu.h | 341 ++++++++++++++++++++ - include/xen/interface/xen.h | 1 + - 10 files changed, 1214 insertions(+) - create mode 100644 drivers/iommu/io-pgtable-xen.c - create mode 100644 drivers/iommu/xen-iommu.c - create mode 100644 include/xen/interface/pv-iommu.h - -diff --git a/arch/x86/include/asm/xen/hypercall.h b/arch/x86/include/asm/xen/hypercall.h -index a16d4631547c..fa7db4d3ba5c 100644 ---- a/arch/x86/include/asm/xen/hypercall.h -+++ b/arch/x86/include/asm/xen/hypercall.h -@@ -497,6 +497,12 @@ HYPERVISOR_xenpmu_op(unsigned int op, void *arg) - return _hypercall2(int, xenpmu_op, op, arg); - } - -+static inline long -+HYPERVISOR_iommu_op(unsigned int subop, void *arg) -+{ -+ return _hypercall2(int, iommu_op, subop, arg); -+} -+ - static inline int - HYPERVISOR_dm_op( - domid_t dom, unsigned int nr_bufs, struct xen_dm_op_buf *bufs) -diff --git a/drivers/iommu/Kconfig b/drivers/iommu/Kconfig -index 70d29b14d851..8ab6cd68bdcc 100644 ---- a/drivers/iommu/Kconfig -+++ b/drivers/iommu/Kconfig -@@ -371,6 +371,16 @@ config VIRTIO_IOMMU - - Say Y here if you intend to run this kernel as a guest. - -+config XEN_IOMMU -+ bool "Xen IOMMU driver" -+ depends on XEN_DOM0 -+ select IOMMU_API -+ select IOMMU_IO_PGTABLE -+ help -+ Xen PV-IOMMU driver for Dom0. -+ -+ Say Y here if you intend to run this guest as Xen Dom0. -+ - config SPRD_IOMMU - tristate "Unisoc IOMMU Support" - depends on ARCH_SPRD || COMPILE_TEST -diff --git a/drivers/iommu/Makefile b/drivers/iommu/Makefile -index 355294fa9033..55954056cbb0 100644 ---- a/drivers/iommu/Makefile -+++ b/drivers/iommu/Makefile -@@ -13,6 +13,7 @@ obj-$(CONFIG_IOMMU_IO_PGTABLE) += io-pgtable.o - obj-$(CONFIG_IOMMU_IO_PGTABLE_ARMV7S) += io-pgtable-arm-v7s.o - obj-$(CONFIG_IOMMU_IO_PGTABLE_LPAE) += io-pgtable-arm.o - obj-$(CONFIG_IOMMU_IO_PGTABLE_DART) += io-pgtable-dart.o -+obj-$(CONFIG_XEN_IOMMU) += io-pgtable-xen.o - obj-$(CONFIG_IOMMU_IOVA) += iova.o - obj-$(CONFIG_OF_IOMMU) += of_iommu.o - obj-$(CONFIG_MSM_IOMMU) += msm_iommu.o -@@ -34,3 +35,4 @@ obj-$(CONFIG_IOMMU_SVA) += iommu-sva.o - obj-$(CONFIG_IOMMU_IOPF) += io-pgfault.o - obj-$(CONFIG_SPRD_IOMMU) += sprd-iommu.o - obj-$(CONFIG_APPLE_DART) += apple-dart.o -+obj-$(CONFIG_XEN_IOMMU) += xen-iommu.o -diff --git a/drivers/iommu/io-pgtable-xen.c b/drivers/iommu/io-pgtable-xen.c -new file mode 100644 -index 000000000000..37a8668a6771 ---- /dev/null -+++ b/drivers/iommu/io-pgtable-xen.c -@@ -0,0 +1,370 @@ -+// SPDX-License-Identifier: GPL-2.0-only -+/* -+ * Generic page table allocator for tracking purposes. -+ * Based on AMD IO pagetable allocator v2. -+ * -+ * Copyright (C) 2024 Vates SAS -+ * Author: Teddy Astie -+ */ -+ -+#define pr_fmt(fmt) "xen-iommu pg-table: " fmt -+#define dev_fmt(fmt) pr_fmt(fmt) -+ -+#include -+#include -+#include -+ -+#include -+ -+#include -+ -+#include "iommu-pages.h" -+ -+#include "xen/page.h" -+ -+#define IOMMU_PAGE_PRESENT BIT_ULL(0) /* Is present */ -+#define IOMMU_PAGE_HUGE BIT_ULL(1) /* Is hugepage */ -+#define MAX_PTRS_PER_PAGE 512 -+ -+#define IOMMU_PAGE_SIZE_2M BIT_ULL(21) -+#define IOMMU_PAGE_SIZE_1G BIT_ULL(30) -+ -+#define PM_ADDR_MASK 0x000ffffffffff000ULL -+#define XEN_IOMMU_PGSIZES (XEN_PAGE_SIZE | (1ULL << 21) | (1ULL << 30)) -+ -+#define PAGE_MODE_NONE 0x00 -+#define PAGE_MODE_1_LEVEL 0x01 -+#define PAGE_MODE_2_LEVEL 0x02 -+#define PAGE_MODE_3_LEVEL 0x03 -+#define PAGE_MODE_4_LEVEL 0x04 -+#define PAGE_MODE_5_LEVEL 0x05 -+ -+#define IOMMU_PTE_PR BIT(0) -+#define IOMMU_PTE_PRESENT(pte) ((pte) & IOMMU_PTE_PR) -+ -+#define PM_LEVEL_SHIFT(x) (12 + ((x) * 9)) -+#define PM_LEVEL_INDEX(x, a) (((a) >> PM_LEVEL_SHIFT((x))) & 0x1ffULL) -+ -+#define IOMMU_IN_ADDR_BIT_SIZE 52 -+#define IOMMU_OUT_ADDR_BIT_SIZE 52 -+ -+#define PAGE_SIZE_ALIGN(address, pagesize) \ -+ ((address) & ~((pagesize) - 1)) -+ -+#define io_pgtable_to_data(x) \ -+ container_of((x), struct xen_io_pgtable, iop) -+ -+#define io_pgtable_ops_to_data(x) \ -+ io_pgtable_to_data(io_pgtable_ops_to_pgtable(x)) -+ -+ -+struct xen_io_pgtable { -+ struct io_pgtable_cfg pgtbl_cfg; -+ struct io_pgtable iop; -+ u64 *pgd; /* pgtable pgd pointer */ -+}; -+ -+static inline bool is_large_pte(u64 pte) -+{ -+ return (pte & IOMMU_PAGE_HUGE); -+} -+ -+static inline u64 set_pgtable_attr(u64 *page) -+{ -+ return (virt_to_phys(page) | IOMMU_PAGE_PRESENT); -+} -+ -+static inline void *get_pgtable_pte(u64 pte) -+{ -+ return phys_to_virt(pte & PM_ADDR_MASK); -+} -+ -+static u64 set_pte_attr(u64 paddr, u64 pg_size) -+{ -+ u64 pte; -+ -+ pte = paddr & PM_ADDR_MASK; -+ pte |= IOMMU_PAGE_PRESENT; -+ -+ /* Large page */ -+ if (pg_size == IOMMU_PAGE_SIZE_1G || pg_size == IOMMU_PAGE_SIZE_2M) -+ pte |= IOMMU_PAGE_HUGE; -+ -+ return pte; -+} -+ -+static inline u64 get_alloc_page_size(u64 size) -+{ -+ if (size >= IOMMU_PAGE_SIZE_1G) -+ return IOMMU_PAGE_SIZE_1G; -+ -+ if (size >= IOMMU_PAGE_SIZE_2M) -+ return IOMMU_PAGE_SIZE_2M; -+ -+ return XEN_PAGE_SIZE; -+} -+ -+static inline int page_size_to_level(u64 pg_size) -+{ -+ if (pg_size == IOMMU_PAGE_SIZE_1G) -+ return PAGE_MODE_3_LEVEL; -+ if (pg_size == IOMMU_PAGE_SIZE_2M) -+ return PAGE_MODE_2_LEVEL; -+ -+ return PAGE_MODE_1_LEVEL; -+} -+ -+static void free_pgtable(u64 *pt, int level) -+{ -+ u64 *p; -+ int i; -+ -+ for (i = 0; i < MAX_PTRS_PER_PAGE; i++) { -+ /* PTE present? */ -+ if (!IOMMU_PTE_PRESENT(pt[i])) -+ continue; -+ -+ if (is_large_pte(pt[i])) -+ continue; -+ -+ /* -+ * Free the next level. No need to look at l1 tables here since -+ * they can only contain leaf PTEs; just free them directly. -+ */ -+ p = get_pgtable_pte(pt[i]); -+ if (level > 2) -+ free_pgtable(p, level - 1); -+ else -+ iommu_free_pages(p); -+ } -+ -+ iommu_free_pages(pt); -+} -+ -+/* Allocate page table */ -+static u64 *xen_alloc_pte(u64 *pgd, unsigned long iova, gfp_t gfp, -+ unsigned long pg_size, bool *updated) -+{ -+ u64 *pte, *page; -+ int level, end_level; -+ -+ level = PAGE_MODE_5_LEVEL - 1; -+ end_level = page_size_to_level(pg_size); -+ pte = &pgd[PM_LEVEL_INDEX(level, iova)]; -+ iova = PAGE_SIZE_ALIGN(iova, XEN_PAGE_SIZE); -+ -+ while (level >= end_level) { -+ u64 __pte, __npte; -+ -+ __pte = *pte; -+ -+ if (IOMMU_PTE_PRESENT(__pte) && is_large_pte(__pte)) { -+ /* Unmap large pte */ -+ cmpxchg64(pte, *pte, 0ULL); -+ *updated = true; -+ continue; -+ } -+ -+ if (!IOMMU_PTE_PRESENT(__pte)) { -+ page = iommu_alloc_pages_sz(gfp, SZ_4K); -+ if (!page) -+ return NULL; -+ -+ __npte = set_pgtable_attr(page); -+ /* pte could have been changed somewhere. */ -+ if (cmpxchg64(pte, __pte, __npte) != __pte) -+ iommu_free_pages(page); -+ else if (IOMMU_PTE_PRESENT(__pte)) -+ *updated = true; -+ -+ continue; -+ } -+ -+ level -= 1; -+ pte = get_pgtable_pte(__pte); -+ pte = &pte[PM_LEVEL_INDEX(level, iova)]; -+ } -+ -+ /* Tear down existing pte entries */ -+ if (IOMMU_PTE_PRESENT(*pte)) { -+ u64 *__pte; -+ -+ *updated = true; -+ __pte = get_pgtable_pte(*pte); -+ cmpxchg64(pte, *pte, 0ULL); -+ if (pg_size == IOMMU_PAGE_SIZE_1G) -+ free_pgtable(__pte, end_level - 1); -+ else if (pg_size == IOMMU_PAGE_SIZE_2M) -+ iommu_free_pages(__pte); -+ } -+ -+ return pte; -+} -+ -+/* -+ * This function checks if there is a PTE for a given dma address. -+ * If there is one, it returns the pointer to it. -+ */ -+static u64 *fetch_pte(struct xen_io_pgtable *pgtable, unsigned long iova, -+ unsigned long *page_size) -+{ -+ u64 *pte; -+ int level; -+ -+ level = PAGE_MODE_5_LEVEL - 1; -+ pte = &pgtable->pgd[PM_LEVEL_INDEX(level, iova)]; -+ /* Default page size is 4K */ -+ *page_size = XEN_PAGE_SIZE; -+ -+ while (level) { -+ /* Not present */ -+ if (!IOMMU_PTE_PRESENT(*pte)) -+ return NULL; -+ -+ /* Walk to the next level */ -+ pte = get_pgtable_pte(*pte); -+ pte = &pte[PM_LEVEL_INDEX(level - 1, iova)]; -+ -+ /* Large page */ -+ if (is_large_pte(*pte)) { -+ if (level == PAGE_MODE_3_LEVEL) -+ *page_size = IOMMU_PAGE_SIZE_1G; -+ else if (level == PAGE_MODE_2_LEVEL) -+ *page_size = IOMMU_PAGE_SIZE_2M; -+ else -+ return NULL; /* Wrongly set PSE bit in PTE */ -+ -+ break; -+ } -+ -+ level -= 1; -+ } -+ -+ return pte; -+} -+ -+static int iommu_xen_map_pages(struct io_pgtable_ops *ops, unsigned long iova, -+ phys_addr_t paddr, size_t pgsize, size_t pgcount, -+ int prot, gfp_t gfp, size_t *mapped) -+{ -+ struct xen_io_pgtable *pgtable = io_pgtable_ops_to_data(ops); -+ struct io_pgtable_cfg *cfg = &pgtable->pgtbl_cfg; -+ u64 *pte; -+ unsigned long map_size; -+ unsigned long mapped_size = 0; -+ size_t size = pgcount << __ffs(pgsize); -+ int ret = 0; -+ bool updated = false; -+ -+ if (WARN_ON(!pgsize || (pgsize & cfg->pgsize_bitmap) != pgsize) || !pgcount) -+ return -EINVAL; -+ -+ while (mapped_size < size) { -+ map_size = get_alloc_page_size(pgsize); -+ pte = xen_alloc_pte(pgtable->pgd, iova, gfp, map_size, &updated); -+ if (!pte) { -+ ret = -ENOMEM; -+ goto out; -+ } -+ -+ *pte = set_pte_attr(paddr, map_size); -+ -+ iova += map_size; -+ paddr += map_size; -+ mapped_size += map_size; -+ } -+ -+out: -+ if (mapped) -+ *mapped += mapped_size; -+ -+ return ret; -+} -+ -+static unsigned long iommu_xen_unmap_pages(struct io_pgtable_ops *ops, -+ unsigned long iova, -+ size_t pgsize, size_t pgcount, -+ struct iommu_iotlb_gather *gather) -+{ -+ struct xen_io_pgtable *pgtable = io_pgtable_ops_to_data(ops); -+ struct io_pgtable_cfg *cfg = &pgtable->iop.cfg; -+ unsigned long unmap_size; -+ unsigned long unmapped = 0; -+ size_t size = pgcount << __ffs(pgsize); -+ u64 *pte; -+ -+ if (WARN_ON(!pgsize || (pgsize & cfg->pgsize_bitmap) != pgsize || !pgcount)) -+ return 0; -+ -+ while (unmapped < size) { -+ pte = fetch_pte(pgtable, iova, &unmap_size); -+ if (!pte) -+ return unmapped; -+ -+ *pte = 0ULL; -+ -+ iova = (iova & ~(unmap_size - 1)) + unmap_size; -+ unmapped += unmap_size; -+ } -+ -+ return unmapped; -+} -+ -+static phys_addr_t iommu_xen_iova_to_phys(struct io_pgtable_ops *ops, unsigned long iova) -+{ -+ struct xen_io_pgtable *pgtable = io_pgtable_ops_to_data(ops); -+ unsigned long offset_mask, pte_pgsize; -+ u64 *pte, __pte; -+ -+ pte = fetch_pte(pgtable, iova, &pte_pgsize); -+ if (!pte || !IOMMU_PTE_PRESENT(*pte)) -+ return 0; -+ -+ offset_mask = pte_pgsize - 1; -+ __pte = *pte & PM_ADDR_MASK; -+ -+ return (__pte & ~offset_mask) | (iova & offset_mask); -+} -+ -+static void xen_free_pgtable(struct io_pgtable *iop) -+{ -+ struct xen_io_pgtable *pgtable = container_of(iop, struct xen_io_pgtable, iop); -+ -+ if (!pgtable || !pgtable->pgd) -+ return; -+ -+ /* Free page table */ -+ free_pgtable(pgtable->pgd, PAGE_MODE_5_LEVEL); -+ kfree(pgtable); -+} -+ -+static struct io_pgtable *xen_alloc_pgtable(struct io_pgtable_cfg *cfg, void *cookie) -+{ -+ struct xen_io_pgtable *pgtable = kmalloc(sizeof(struct xen_io_pgtable), -+ GFP_KERNEL); -+ if (!pgtable) -+ return NULL; -+ -+ pgtable->pgd = iommu_alloc_pages_sz(GFP_KERNEL, SZ_4K); -+ if (!pgtable->pgd) { -+ kfree(pgtable); -+ return NULL; -+ } -+ -+ pgtable->iop.ops.map_pages = iommu_xen_map_pages; -+ pgtable->iop.ops.unmap_pages = iommu_xen_unmap_pages; -+ pgtable->iop.ops.iova_to_phys = iommu_xen_iova_to_phys; -+ -+ cfg->pgsize_bitmap = XEN_IOMMU_PGSIZES; -+ cfg->ias = IOMMU_IN_ADDR_BIT_SIZE; -+ cfg->oas = IOMMU_OUT_ADDR_BIT_SIZE; -+ -+ pgtable->pgtbl_cfg = *cfg; -+ -+ return &pgtable->iop; -+} -+ -+struct io_pgtable_init_fns io_pgtable_xen_init_fns = { -+ .alloc = xen_alloc_pgtable, -+ .free = xen_free_pgtable, -+}; -diff --git a/drivers/iommu/io-pgtable.c b/drivers/iommu/io-pgtable.c -index 8841c1487f00..763f6c34db73 100644 ---- a/drivers/iommu/io-pgtable.c -+++ b/drivers/iommu/io-pgtable.c -@@ -32,6 +32,9 @@ io_pgtable_init_table[IO_PGTABLE_NUM_FMTS] = { - [AMD_IOMMU_V1] = &io_pgtable_amd_iommu_v1_init_fns, - [AMD_IOMMU_V2] = &io_pgtable_amd_iommu_v2_init_fns, - #endif -+#ifdef CONFIG_XEN_IOMMU -+ [XEN_IOMMU_GENERIC] = &io_pgtable_xen_init_fns, -+#endif - }; - - static int check_custom_allocator(enum io_pgtable_fmt fmt, -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -new file mode 100644 -index 000000000000..1ea66aeda096 ---- /dev/null -+++ b/drivers/iommu/xen-iommu.c -@@ -0,0 +1,446 @@ -+// SPDX-License-Identifier: GPL-2.0 -+/* -+ * Xen PV-IOMMU driver. -+ * -+ * Copyright (C) 2024 Vates SAS -+ * -+ * Author: Teddy Astie -+ * -+ */ -+ -+#define pr_fmt(fmt) "xen-iommu: " fmt -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+#include -+#include -+#include -+#include -+#include -+#include -+ -+MODULE_DESCRIPTION("Xen IOMMU driver"); -+MODULE_AUTHOR("Teddy Astie "); -+MODULE_LICENSE("GPL"); -+ -+#define MSI_RANGE_START (0xfee00000) -+#define MSI_RANGE_END (0xfeefffff) -+ -+struct xen_iommu_domain { -+ struct iommu_domain domain; -+ -+ u16 ctx_no; /* Xen PV-IOMMU context number */ -+ struct io_pgtable_ops *pgtable; /* Parralel page table for iova_to_phys */ -+}; -+ -+static struct iommu_device xen_iommu_device; -+static struct pv_iommu_capabilities caps; -+ -+static struct xen_iommu_domain xen_iommu_identity_domain; -+static unsigned long xen_iommu_pgsize_bitmap = XEN_PAGE_SIZE; -+static bool map_single_pages = false; -+ -+static inline struct xen_iommu_domain *to_xen_iommu_domain(struct iommu_domain *dom) -+{ -+ return container_of(dom, struct xen_iommu_domain, domain); -+} -+ -+static inline u64 addr_to_pfn(u64 addr) -+{ -+ return addr >> 12; -+} -+ -+static inline u64 pfn_to_addr(u64 pfn) -+{ -+ return pfn << 12; -+} -+ -+static bool xen_iommu_capable(struct device *dev, enum iommu_cap cap) -+{ -+ switch (cap) { -+ case IOMMU_CAP_CACHE_COHERENCY: -+ return true; -+ -+ default: -+ return false; -+ } -+} -+ -+static struct iommu_domain *xen_iommu_domain_alloc_paging(struct device *dev) -+{ -+ struct xen_iommu_domain *domain; -+ struct io_pgtable_cfg cfg = { .alloc = NULL, .free = NULL }; -+ struct io_pgtable_ops *pgtable; -+ int ret; -+ -+ struct pv_iommu_alloc alloc = { .alloc_flags = 0 }; -+ -+ domain = kzalloc(sizeof(*domain), GFP_KERNEL); -+ if (!domain) -+ return ERR_PTR(-ENOMEM); -+ -+ pgtable = alloc_io_pgtable_ops(XEN_IOMMU_GENERIC, &cfg, NULL); -+ if (!pgtable) { -+ kfree(domain); -+ return ERR_PTR(-ENOMEM); -+ } -+ -+ ret = HYPERVISOR_iommu_op(IOMMU_alloc_context, &alloc); -+ -+ if (ret) { -+ pr_err("Unable to create Xen IOMMU context (%d)", ret); -+ kfree(domain); -+ free_io_pgtable_ops(pgtable); -+ return ERR_PTR(ret); -+ } -+ -+ domain->ctx_no = alloc.ctx_no; -+ domain->pgtable = pgtable; -+ -+ domain->domain.pgsize_bitmap = xen_iommu_pgsize_bitmap; -+ domain->domain.geometry = (struct iommu_domain_geometry){ -+ .aperture_start = 0, -+ .aperture_end = caps.max_iova_addr, -+ .force_aperture = true, -+ }; -+ -+ return &domain->domain; -+} -+ -+static struct iommu_device *xen_iommu_probe_device(struct device *dev) -+{ -+ if (!dev_is_pci(dev)) -+ return ERR_PTR(-ENODEV); -+ -+ return &xen_iommu_device; -+} -+ -+static int xen_iommu_map_pages(struct iommu_domain *domain, unsigned long iova, -+ phys_addr_t paddr, size_t pgsize, size_t pgcount, -+ int prot, gfp_t gfp, size_t *mapped) -+{ -+ int ret = 0; -+ size_t _mapped; /* for pgtable callback */ -+ struct xen_iommu_domain *dom = to_xen_iommu_domain(domain); -+ struct pv_iommu_map_pages map = { -+ .ctx_no = dom->ctx_no, -+ .pgsize = pgsize, -+ .map_flags = 0, -+ .mapped = 0, -+ }; -+ -+ /* NOTE: paddr is actually bound to pfn, not gfn */ -+ uint64_t pfn0 = addr_to_pfn(paddr); -+ uint64_t dfn0 = addr_to_pfn(iova); -+ -+ if (prot & IOMMU_READ) -+ map.map_flags |= IOMMU_MAP_readable; -+ -+ if (prot & IOMMU_WRITE) -+ map.map_flags |= IOMMU_MAP_writeable; -+ -+ if (prot & IOMMU_CACHE) -+ map.map_flags |= IOMMU_MAP_cache; -+ -+ if (map_single_pages) { -+ size_t i = 0; -+ map.nr_pages = 1; -+ -+ for (; i < pgcount; i++) { -+ map.gfn = pfn_to_gfn(pfn0 + i); -+ map.dfn = dfn0 + i; -+ map.nr_pages = 1; -+ -+ ret = HYPERVISOR_iommu_op(IOMMU_map_pages, &map); -+ -+ if (ret) -+ break; -+ } -+ } else { -+ map.nr_pages = pgcount; -+ map.gfn = pfn_to_gfn(pfn0); -+ map.dfn = dfn0; -+ -+ ret = HYPERVISOR_iommu_op(IOMMU_map_pages, &map); -+ } -+ -+ if (mapped) -+ *mapped = pgsize * map.mapped; -+ -+ dom->pgtable->map_pages(dom->pgtable, iova, paddr, pgsize, pgcount, -+ prot, gfp, &_mapped); -+ -+ return ret; -+} -+ -+static size_t xen_iommu_unmap_pages(struct iommu_domain *domain, unsigned long iova, -+ size_t pgsize, size_t pgcount, -+ struct iommu_iotlb_gather *iotlb_gather) -+{ -+ struct xen_iommu_domain *dom = to_xen_iommu_domain(domain); -+ struct pv_iommu_unmap_pages unmap = { -+ .ctx_no = dom->ctx_no, -+ .pgsize = pgsize, -+ .unmapped = 0, -+ .nr_pages = pgcount, -+ .dfn = addr_to_pfn(iova), -+ }; -+ -+ WARN_ON(HYPERVISOR_iommu_op(IOMMU_unmap_pages, &unmap)); -+ dom->pgtable->unmap_pages(dom->pgtable, iova, pgsize, pgcount, -+ iotlb_gather); -+ -+ return unmap.unmapped * pgsize; -+} -+ -+static int xen_iommu_attach_dev(struct iommu_domain *domain, struct device *dev) -+{ -+ struct pci_dev *pdev; -+ struct xen_iommu_domain *dom = to_xen_iommu_domain(domain); -+ struct pv_iommu_reattach_device reattach = { -+ .ctx_no = dom->ctx_no, -+ .pasid = 0, -+ }; -+ -+ pdev = to_pci_dev(dev); -+ -+ reattach.dev.seg = pci_domain_nr(pdev->bus); -+ reattach.dev.bus = pdev->bus->number; -+ reattach.dev.devfn = pdev->devfn; -+ -+ return HYPERVISOR_iommu_op(IOMMU_reattach_device, &reattach); -+} -+ -+static void xen_iommu_free(struct iommu_domain *domain) -+{ -+ int ret; -+ struct xen_iommu_domain *dom = to_xen_iommu_domain(domain); -+ struct pv_iommu_free op = { -+ .ctx_no = dom->ctx_no, -+ .free_flags = 0, -+ }; -+ -+ ret = HYPERVISOR_iommu_op(IOMMU_free_context, &op); -+ -+ if (ret) -+ pr_err("Context %hu destruction failure\n", dom->ctx_no); -+ -+ free_io_pgtable_ops(dom->pgtable); -+ -+ kfree(domain); -+} -+ -+static phys_addr_t xen_iommu_iova_to_phys(struct iommu_domain *domain, dma_addr_t iova) -+{ -+ struct xen_iommu_domain *dom = to_xen_iommu_domain(domain); -+ -+ if (!dom->ctx_no) -+ /* If default domain is identity, phys_addr is iova. */ -+ return (caps.cap_flags & IOMMUCAP_default_identity) ? iova : 0; -+ -+ return dom->pgtable->iova_to_phys(dom->pgtable, iova); -+} -+ -+static void xen_iommu_get_resv_regions(struct device *dev, struct list_head *head) -+{ -+ struct iommu_resv_region *reg; -+ struct xen_reserved_device_memory *entries; -+ struct xen_reserved_device_memory_map map; -+ struct pci_dev *pdev; -+ int ret, i; -+ -+ pdev = to_pci_dev(dev); -+ -+ reg = iommu_alloc_resv_region(MSI_RANGE_START, -+ MSI_RANGE_END - MSI_RANGE_START + 1, -+ 0, IOMMU_RESV_MSI, GFP_KERNEL); -+ -+ if (!reg) -+ return; -+ -+ list_add_tail(®->list, head); -+ -+ /* Map xen-specific entries */ -+ -+ /* First, get number of entries to map */ -+ map.buffer = NULL; -+ map.nr_entries = 0; -+ map.flags = 0; -+ -+ map.dev.pci.seg = pci_domain_nr(pdev->bus); -+ map.dev.pci.bus = pdev->bus->number; -+ map.dev.pci.devfn = pdev->devfn; -+ -+ ret = HYPERVISOR_memory_op(XENMEM_reserved_device_memory_map, &map); -+ -+ if (ret == 0) -+ /* No reserved region, nothing to do */ -+ return; -+ -+ if (ret != -ENOBUFS) { -+ pr_err("Unable to get reserved region count (%d)\n", ret); -+ return; -+ } -+ -+ /* Assume a reasonable number of entries, otherwise, something is probably wrong */ -+ if (WARN_ON(map.nr_entries > 256)) -+ pr_warn("Xen reporting many reserved regions (%u)\n", map.nr_entries); -+ -+ /* And finally get actual mappings */ -+ entries = kcalloc(map.nr_entries, sizeof(struct xen_reserved_device_memory), -+ GFP_KERNEL); -+ -+ if (!entries) { -+ pr_err("No memory for map entries\n"); -+ return; -+ } -+ -+ map.buffer = entries; -+ -+ ret = HYPERVISOR_memory_op(XENMEM_reserved_device_memory_map, &map); -+ -+ if (ret != 0) { -+ pr_err("Unable to get reserved regions (%d)\n", ret); -+ kfree(entries); -+ return; -+ } -+ -+ for (i = 0; i < map.nr_entries; i++) { -+ struct xen_reserved_device_memory entry = entries[i]; -+ -+ reg = iommu_alloc_resv_region(pfn_to_addr(entry.start_pfn), -+ pfn_to_addr(entry.nr_pages), -+ 0, IOMMU_RESV_RESERVED, GFP_KERNEL); -+ -+ if (!reg) -+ break; -+ -+ list_add_tail(®->list, head); -+ } -+ -+ kfree(entries); -+} -+ -+static struct iommu_ops xen_iommu_ops = { -+ .identity_domain = &xen_iommu_identity_domain.domain, -+ .release_domain = &xen_iommu_identity_domain.domain, -+ .capable = xen_iommu_capable, -+ .domain_alloc_paging = xen_iommu_domain_alloc_paging, -+ .probe_device = xen_iommu_probe_device, -+ .device_group = pci_device_group, -+ .get_resv_regions = xen_iommu_get_resv_regions, -+ .default_domain_ops = &(const struct iommu_domain_ops) { -+ .map_pages = xen_iommu_map_pages, -+ .unmap_pages = xen_iommu_unmap_pages, -+ .attach_dev = xen_iommu_attach_dev, -+ .iova_to_phys = xen_iommu_iova_to_phys, -+ .free = xen_iommu_free, -+ }, -+}; -+ -+static int __init xen_iommu_init(void) -+{ -+ long ret; -+ -+ if (!xen_domain()) -+ return -ENODEV; -+ -+ /* Check if iommu_op is supported */ -+ if ((ret = HYPERVISOR_iommu_op(IOMMU_query_capabilities, &caps))) -+ { -+ pr_err("Unable to query capabilities (%ld)", ret); -+ return -ENODEV; /* No Xen IOMMU hardware */ -+ } -+ -+ /* If ctx_no is zero, it may be due to PV-IOMMU not being initialized. */ -+ if (!caps.max_ctx_no) -+ { -+ /* Try to initialize PV-IOMMU */ -+ struct pv_iommu_init init; -+ -+ pr_info("Got no usable context, try initializing PV-IOMMU\n"); -+ -+ /* FIXME: Don't hardcode this */ -+ init.max_ctx_no = 128; -+ init.arena_order = 12; -+ -+ pr_info("init.max_ctx_no=%hu\n", init.max_ctx_no); -+ pr_info("init.arena_order=%hu\n", init.arena_order); -+ -+ /* Try to initialize PV-IOMMU */ -+ ret = HYPERVISOR_iommu_op(IOMMU_init, &init); -+ -+ if (ret == -EACCES) { -+ /* PV-IOMMU being already initialized often means not allowed. */ -+ pr_warn("PV-IOMMU is already initialized, guest may not be allowed to use PV-IOMMU\n"); -+ return -EACCES; -+ } else if (ret) { -+ pr_err("PV-IOMMU initialization failure (%ld)", ret); -+ return ret; -+ } -+ -+ WARN_ON(HYPERVISOR_iommu_op(IOMMU_query_capabilities, &caps)); -+ } -+ -+ pr_info("Initialising Xen IOMMU driver\n"); -+ pr_info("max_ctx_no=%hu\n", caps.max_ctx_no); -+ pr_info("max_iova_addr=%llx\n", caps.max_iova_addr); -+ pr_info("pgsize_mask=%d\n", caps.pgsize_mask); -+ pr_info("default_identity=%c\n", (caps.cap_flags & IOMMUCAP_default_identity) ? 'y' : 'n'); -+ pr_info("cache=%c\n", (caps.cap_flags & IOMMUCAP_cache) ? 'y' : 'n'); -+ -+ if (caps.max_ctx_no == 0) { -+ pr_err("Unable to use IOMMU PV driver (no context available ?)\n"); -+ return -ENOTSUPP; /* Unable to use IOMMU PV ? */ -+ } -+ -+ xen_iommu_pgsize_bitmap = caps.pgsize_mask; -+ -+ if (xen_domain_type == XEN_PV_DOMAIN) -+ /* TODO: In PV domain, due to the existing pfn-gfn mapping we need to -+ * consider that under certains circonstances, we have : -+ * pfn_to_gfn(x + 1) != pfn_to_gfn(x) + 1 -+ * -+ * In these cases, we would want to separate the subop into several calls. -+ * (only doing the grouped operation when the mapping is actually contigous) -+ * Only map operation would be affected, as unmap actually uses dfn which -+ * doesn't have this kind of mapping. -+ * -+ * Force single-page operations to work arround this issue for now. -+ */ -+ map_single_pages = true; -+ -+ /* Initialize identity domain */ -+ xen_iommu_identity_domain.ctx_no = 0; -+ -+ xen_iommu_identity_domain.domain.pgsize_bitmap = xen_iommu_pgsize_bitmap; -+ xen_iommu_identity_domain.domain.geometry = (struct iommu_domain_geometry){ -+ .aperture_start = 0, -+ .aperture_end = caps.max_iova_addr, -+ .force_aperture = true, -+ }; -+ -+ ret = iommu_device_sysfs_add(&xen_iommu_device, NULL, NULL, "xen-iommu"); -+ if (ret) { -+ pr_err("Unable to add Xen IOMMU sysfs\n"); -+ return ret; -+ } -+ -+ ret = iommu_device_register(&xen_iommu_device, &xen_iommu_ops, NULL); -+ if (ret) { -+ pr_err("Unable to register Xen IOMMU device %ld\n", ret); -+ iommu_device_sysfs_remove(&xen_iommu_device); -+ return ret; -+ } -+ -+ return 0; -+} -+ -+module_init(xen_iommu_init); -diff --git a/include/linux/io-pgtable.h b/include/linux/io-pgtable.h -index 8a823c6f2b4a..7814114f0537 100644 ---- a/include/linux/io-pgtable.h -+++ b/include/linux/io-pgtable.h -@@ -19,6 +19,7 @@ enum io_pgtable_fmt { - AMD_IOMMU_V2, - APPLE_DART, - APPLE_DART2, -+ XEN_IOMMU_GENERIC, - IO_PGTABLE_NUM_FMTS, - }; - -@@ -325,5 +326,6 @@ extern struct io_pgtable_init_fns io_pgtable_arm_mali_lpae_init_fns; - extern struct io_pgtable_init_fns io_pgtable_amd_iommu_v1_init_fns; - extern struct io_pgtable_init_fns io_pgtable_amd_iommu_v2_init_fns; - extern struct io_pgtable_init_fns io_pgtable_apple_dart_init_fns; -+extern struct io_pgtable_init_fns io_pgtable_xen_init_fns; - - #endif /* __IO_PGTABLE_H */ -diff --git a/include/xen/interface/memory.h b/include/xen/interface/memory.h -index 1a371a825c55..c860acaf4b0e 100644 ---- a/include/xen/interface/memory.h -+++ b/include/xen/interface/memory.h -@@ -10,6 +10,7 @@ - #ifndef __XEN_PUBLIC_MEMORY_H__ - #define __XEN_PUBLIC_MEMORY_H__ - -+#include - #include - - /* -@@ -214,6 +215,38 @@ struct xen_add_to_physmap_range { - }; - DEFINE_GUEST_HANDLE_STRUCT(xen_add_to_physmap_range); - -+/* -+ * With some legacy devices, certain guest-physical addresses cannot safely -+ * be used for other purposes, e.g. to map guest RAM. This hypercall -+ * enumerates those regions so the toolstack can avoid using them. -+ */ -+#define XENMEM_reserved_device_memory_map 27 -+struct xen_reserved_device_memory { -+ xen_pfn_t start_pfn; -+ xen_ulong_t nr_pages; -+}; -+DEFINE_GUEST_HANDLE_STRUCT(xen_reserved_device_memory); -+ -+struct xen_reserved_device_memory_map { -+#define XENMEM_RDM_ALL 1 /* Request all regions (ignore dev union). */ -+ /* IN */ -+ uint32_t flags; -+ /* -+ * IN/OUT -+ * -+ * Gets set to the required number of entries when too low, -+ * signaled by error code -ERANGE. -+ */ -+ unsigned int nr_entries; -+ /* OUT */ -+ GUEST_HANDLE(xen_reserved_device_memory) buffer; -+ /* IN */ -+ union { -+ struct physdev_pci_device pci; -+ } dev; -+}; -+DEFINE_GUEST_HANDLE_STRUCT(xen_reserved_device_memory_map); -+ - /* - * Returns the pseudo-physical memory map as it was when the domain - * was started (specified by XENMEM_set_memory_map). -diff --git a/include/xen/interface/pv-iommu.h b/include/xen/interface/pv-iommu.h -new file mode 100644 -index 000000000000..a4a470319486 ---- /dev/null -+++ b/include/xen/interface/pv-iommu.h -@@ -0,0 +1,341 @@ -+/* SPDX-License-Identifier: MIT */ -+/** -+ * pv-iommu.h -+ * -+ * Paravirtualized IOMMU driver interface. -+ * -+ * Copyright (c) 2024 Teddy Astie -+ */ -+ -+#ifndef __XEN_PUBLIC_PV_IOMMU_H__ -+#define __XEN_PUBLIC_PV_IOMMU_H__ -+ -+#include "xen.h" -+#include "physdev.h" -+ -+#ifndef uint64_aligned_t -+#define uint64_aligned_t uint64_t -+#endif -+ -+#define IOMMU_DEFAULT_CONTEXT (0) -+ -+enum { -+ /* Basic cmd */ -+ IOMMU_noop = 0, -+ IOMMU_query_capabilities, -+ IOMMU_init, -+ IOMMU_alloc_context, -+ IOMMU_free_context, -+ IOMMU_reattach_device, -+ IOMMU_map_pages, -+ IOMMU_unmap_pages, -+ IOMMU_remote_cmd, -+ -+ /* Extended cmd */ -+ IOMMU_alloc_nested, /* if IOMMUCAP_nested */ -+ IOMMU_flush_nested, /* if IOMMUCAP_nested */ -+ IOMMU_attach_pasid, /* if IOMMUCAP_pasid */ -+ IOMMU_detach_pasid, /* if IOMMUCAP_pasid */ -+}; -+ -+/** -+ * Indicate if the default context is a identity mapping to domain memory. -+ * If not defined, default context blocks all DMA to domain memory. -+ */ -+#define IOMMUCAP_default_identity (1 << 0) -+ -+/** -+ * IOMMU_MAP_cache support. -+ */ -+#define IOMMUCAP_cache (1 << 1) -+ -+/** -+ * Support for IOMMU_alloc_nested. -+ */ -+#define IOMMUCAP_nested (1 << 2) -+ -+/** -+ * Support for IOMMU_attach_pasid and IOMMU_detach_pasid and pasid parameter in -+ * reattach_context. -+ */ -+#define IOMMUCAP_pasid (1 << 3) -+ -+/** -+ * Support for IOMMU_ALLOC_identity -+ */ -+#define IOMMUCAP_identity (1 << 4) -+ -+/** -+ * IOMMU_query_capabilities -+ * Query PV-IOMMU capabilities for this domain. -+ */ -+struct pv_iommu_capabilities { -+ /* -+ * OUT: Maximum device address (iova) that the guest can use for mappings. -+ */ -+ uint64_aligned_t max_iova_addr; -+ -+ /* OUT: IOMMU capabilities flags */ -+ uint32_t cap_flags; -+ -+ /* OUT: Mask of all supported page sizes. */ -+ uint32_t pgsize_mask; -+ -+ /* OUT: Maximum pasid (if IOMMUCAP_pasid) */ -+ uint32_t max_pasid; -+ -+ /* OUT: Maximum number of IOMMU context this domain can use. */ -+ uint16_t max_ctx_no; -+}; -+typedef struct pv_iommu_capabilities pv_iommu_capabilities_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_capabilities_t); -+ -+/** -+ * IOMMU_init -+ * Initialize PV-IOMMU for this domain. -+ * -+ * Fails with -EACCESS if PV-IOMMU is already initialized. -+ */ -+struct pv_iommu_init { -+ /* IN: Maximum number of IOMMU context this domain can use. */ -+ uint32_t max_ctx_no; -+ -+ /* IN: Arena size in pages (in power of two) */ -+ uint32_t arena_order; -+}; -+typedef struct pv_iommu_init pv_iommu_init_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_init_t); -+ -+/** -+ * Create a 1:1 identity mapped context to domain memory -+ * (needs IOMMUCAP_identity). -+ */ -+#define IOMMU_ALLOC_identity (1 << 0) -+ -+/** -+ * IOMMU_alloc_context -+ * Allocate an IOMMU context. -+ * Fails with -ENOSPC if no context number is available. -+ */ -+struct pv_iommu_alloc { -+ /* OUT: allocated IOMMU context number */ -+ uint16_t ctx_no; -+ -+ /* IN: allocation flags */ -+ uint32_t alloc_flags; -+}; -+typedef struct pv_iommu_alloc pv_iommu_alloc_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_alloc_t); -+ -+/** -+ * Move all devices to default context before freeing the context. -+ */ -+#define IOMMU_FREE_reattach_default (1 << 0) -+ -+/** -+ * IOMMU_free_context -+ * Destroy a IOMMU context. -+ * -+ * If IOMMU_FREE_reattach_default is specified, move all context devices to -+ * default context before destroying this context. -+ * -+ * If there are devices in the context and IOMMU_FREE_reattach_default is not -+ * specified, fail with -EBUSY. -+ * -+ * The default context can't be destroyed. -+ */ -+struct pv_iommu_free { -+ /* IN: IOMMU context number to free */ -+ uint16_t ctx_no; -+ -+ /* IN: Free operation specific flags */ -+ uint32_t free_flags; -+}; -+typedef struct pv_iommu_free pv_iommu_free_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_free_t); -+ -+/* Device has read access */ -+#define IOMMU_MAP_readable (1 << 0) -+ -+/* Device has write access */ -+#define IOMMU_MAP_writeable (1 << 1) -+ -+/* Enforce DMA coherency */ -+#define IOMMU_MAP_cache (1 << 2) -+ -+/** -+ * IOMMU_map_pages -+ * Map pages on a IOMMU context. -+ * -+ * pgsize must be supported by pgsize_mask. -+ * Fails with -EINVAL if mapping on top of another mapping. -+ * Report actually mapped page count in mapped field (regardless of failure). -+ */ -+struct pv_iommu_map_pages { -+ /* IN: IOMMU context number */ -+ uint16_t ctx_no; -+ -+ /* IN: Guest frame number */ -+ uint64_aligned_t gfn; -+ -+ /* IN: Device frame number */ -+ uint64_aligned_t dfn; -+ -+ /* IN: Map flags */ -+ uint32_t map_flags; -+ -+ /* IN: Size of pages to map */ -+ uint32_t pgsize; -+ -+ /* IN: Number of pages to map */ -+ uint32_t nr_pages; -+ -+ /* OUT: Number of pages actually mapped */ -+ uint32_t mapped; -+}; -+typedef struct pv_iommu_map_pages pv_iommu_map_pages_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_map_pages_t); -+ -+/** -+ * IOMMU_unmap_pages -+ * Unmap pages on a IOMMU context. -+ * -+ * pgsize must be supported by pgsize_mask. -+ * Report actually unmapped page count in mapped field (regardless of failure). -+ * Fails with -ENOENT when attempting to unmap a page without any mapping -+ */ -+struct pv_iommu_unmap_pages { -+ /* IN: IOMMU context number */ -+ uint16_t ctx_no; -+ -+ /* IN: Device frame number */ -+ uint64_aligned_t dfn; -+ -+ /* IN: Size of pages to unmap */ -+ uint32_t pgsize; -+ -+ /* IN: Number of pages to unmap */ -+ uint32_t nr_pages; -+ -+ /* OUT: Number of pages actually unmapped */ -+ uint32_t unmapped; -+}; -+typedef struct pv_iommu_unmap_pages pv_iommu_unmap_pages_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_unmap_pages_t); -+ -+/** -+ * IOMMU_reattach_device -+ * Reattach a device to another IOMMU context. -+ * Fails with -ENODEV if no such device exist. -+ */ -+struct pv_iommu_reattach_device { -+ /* IN: Target IOMMU context number */ -+ uint16_t ctx_no; -+ -+ /* IN: Physical device to move */ -+ struct physdev_pci_device dev; -+ -+ /* IN: PASID of the device (if IOMMUCAP_pasid) */ -+ uint32_t pasid; -+}; -+typedef struct pv_iommu_reattach_device pv_iommu_reattach_device_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_reattach_device_t); -+ -+ -+/** -+ * IOMMU_remote_cmd -+ * Do a PV-IOMMU operation on another domain. -+ * Current domain needs to be allowed to act on the target domain, otherwise -+ * fails with -EPERM. -+ */ -+struct pv_iommu_remote_cmd { -+ /* IN: Target domain to do the subop on */ -+ uint16_t domid; -+ -+ /* IN: Command to do on target domain. */ -+ uint16_t subop; -+ -+ /* INOUT: Command argument from current domain memory */ -+ GUEST_HANDLE(void) arg; -+}; -+typedef struct pv_iommu_remote_cmd pv_iommu_remote_cmd_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_remote_cmd_t); -+ -+/** -+ * IOMMU_alloc_nested -+ * Create a nested IOMMU context (needs IOMMUCAP_nested). -+ * -+ * This context uses a platform-specific page table from domain address space -+ * specified in pgtable_gfn and use it for nested translations. -+ * -+ * Explicit flushes needs to be submited with IOMMU_flush_nested on -+ * modification of the nested pagetable to ensure coherency between IOTLB and -+ * nested page table. -+ * -+ * This context can be destroyed using IOMMU_free_context. -+ * This context cannot be modified using map_pages, unmap_pages. -+ */ -+struct pv_iommu_alloc_nested { -+ /* OUT: allocated IOMMU context number */ -+ uint16_t ctx_no; -+ -+ /* IN: guest frame number of the nested page table */ -+ uint64_aligned_t pgtable_gfn; -+ -+ /* IN: nested mode flags */ -+ uint64_aligned_t nested_flags; -+}; -+typedef struct pv_iommu_alloc_nested pv_iommu_alloc_nested_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_alloc_nested_t); -+ -+/** -+ * IOMMU_flush_nested (needs IOMMUCAP_nested) -+ * Flush the IOTLB for nested translation. -+ */ -+struct pv_iommu_flush_nested { -+ /* TODO */ -+}; -+typedef struct pv_iommu_flush_nested pv_iommu_flush_nested_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_flush_nested_t); -+ -+/** -+ * IOMMU_attach_pasid (needs IOMMUCAP_pasid) -+ * Attach a new device-with-pasid to a IOMMU context. -+ * If a matching device-with-pasid already exists (globally), -+ * fail with -EEXIST. -+ * If pasid is 0, fails with -EINVAL. -+ * If physical device doesn't exist in domain, fail with -ENOENT. -+ */ -+struct pv_iommu_attach_pasid { -+ /* IN: IOMMU context to add the device-with-pasid in */ -+ uint16_t ctx_no; -+ -+ /* IN: Physical device */ -+ struct physdev_pci_device dev; -+ -+ /* IN: pasid of the device to attach */ -+ uint32_t pasid; -+}; -+typedef struct pv_iommu_attach_pasid pv_iommu_attach_pasid_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_attach_pasid_t); -+ -+/** -+ * IOMMU_detach_pasid (needs IOMMUCAP_pasid) -+ * detach a device-with-pasid. -+ * If the device-with-pasid doesn't exist or belong to the domain, -+ * fail with -ENOENT. -+ * If pasid is 0, fails with -EINVAL. -+ */ -+struct pv_iommu_detach_pasid { -+ /* IN: Physical device */ -+ struct physdev_pci_device dev; -+ -+ /* pasid of the device to detach */ -+ uint32_t pasid; -+}; -+typedef struct pv_iommu_detach_pasid pv_iommu_detach_pasid_t; -+DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_detach_pasid_t); -+ -+/* long do_iommu_op(int subop, XEN_GUEST_HANDLE_PARAM(void) arg) */ -+ -+#endif -\ No newline at end of file -diff --git a/include/xen/interface/xen.h b/include/xen/interface/xen.h -index 0ca23eca2a9c..8b1daf3fecc6 100644 ---- a/include/xen/interface/xen.h -+++ b/include/xen/interface/xen.h -@@ -65,6 +65,7 @@ - #define __HYPERVISOR_xc_reserved_op 39 /* reserved for XenClient */ - #define __HYPERVISOR_xenpmu_op 40 - #define __HYPERVISOR_dm_op 41 -+#define __HYPERVISOR_iommu_op 43 - - /* Architecture-specific hypercall definitions. */ - #define __HYPERVISOR_arch_0 48 --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0002-iommu-xen-Allow-the-PV-IOMMU-driver-in-a-guest.patch b/patches/pv-iommu-6.18/0002-iommu-xen-Allow-the-PV-IOMMU-driver-in-a-guest.patch deleted file mode 100644 index 6dcfadce..00000000 --- a/patches/pv-iommu-6.18/0002-iommu-xen-Allow-the-PV-IOMMU-driver-in-a-guest.patch +++ /dev/null @@ -1,69 +0,0 @@ -From ba74c369c799713f6d47b9fee45ca3d16d09bb52 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Wed, 2 Sep 2026 00:44:53 -0700 -Subject: [PATCH 02/11] iommu/xen: Allow the PV-IOMMU driver in a guest - -The driver itself was never Dom0-specific -- it only checks xen_domain() -and sends the SBDF it sees, which is what the hypervisor now translates. -The restriction lived entirely in Kconfig, so drop it to depends on XEN. - -Guests do need one adjustment. Dom0 sees the real topology, bridges -included, and Xen tracks all of it. A guest only gets the endpoints vPCI -assigned to it, sitting under an emulated bridge that has no device on the -Xen side, so don't claim bridges there; attaching a context to one can only -fail. - -A guest also has to call IOMMU_init before it has any contexts to allocate, -which the driver already does unconditionally. ---- - drivers/iommu/Kconfig | 8 +++++--- - drivers/iommu/xen-iommu.c | 10 ++++++++++ - 2 files changed, 15 insertions(+), 3 deletions(-) - -diff --git a/drivers/iommu/Kconfig b/drivers/iommu/Kconfig -index 8ab6cd68bdcc..2e1e4a72670e 100644 ---- a/drivers/iommu/Kconfig -+++ b/drivers/iommu/Kconfig -@@ -373,13 +373,15 @@ config VIRTIO_IOMMU - - config XEN_IOMMU - bool "Xen IOMMU driver" -- depends on XEN_DOM0 -+ depends on XEN - select IOMMU_API - select IOMMU_IO_PGTABLE - help -- Xen PV-IOMMU driver for Dom0. -+ Xen PV-IOMMU driver. - -- Say Y here if you intend to run this guest as Xen Dom0. -+ Say Y here if you intend to run this kernel under Xen, either as -+ Dom0 or as a guest whose passed-through devices come through -+ vPCI. - - config SPRD_IOMMU - tristate "Unisoc IOMMU Support" -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index 1ea66aeda096..35cbc7d7026b 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -119,6 +119,16 @@ static struct iommu_device *xen_iommu_probe_device(struct device *dev) - if (!dev_is_pci(dev)) - return ERR_PTR(-ENODEV); - -+ /* -+ * Dom0 sees the real topology, bridges included, and Xen tracks all of -+ * it. A guest only gets the endpoints vPCI assigned to it; the bridge -+ * above them is emulated and has no device for Xen to attach a context -+ * to, so leave it alone rather than fail the attach later. -+ */ -+ if (!xen_initial_domain() && -+ to_pci_dev(dev)->hdr_type != PCI_HEADER_TYPE_NORMAL) -+ return ERR_PTR(-ENODEV); -+ - return &xen_iommu_device; - } - --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0003-iommu-xen-Give-the-identity-domain-a-type-and-ops.patch b/patches/pv-iommu-6.18/0003-iommu-xen-Give-the-identity-domain-a-type-and-ops.patch deleted file mode 100644 index 952fd10f..00000000 --- a/patches/pv-iommu-6.18/0003-iommu-xen-Give-the-identity-domain-a-type-and-ops.patch +++ /dev/null @@ -1,50 +0,0 @@ -From 9d31f2f48b7b6fec0f2f006fd0998e8014e2d672 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Wed, 2 Sep 2026 10:08:58 -0700 -Subject: [PATCH 03/11] iommu/xen: Give the identity domain a type and ops - -xen_iommu_identity_domain is statically allocated, and the core only fills -in type and ops for domains it allocated itself. Left at zero the type is -IOMMU_DOMAIN_BLOCKED, not identity, and the ops pointer is NULL, so -anything the core routes to this domain either does the wrong thing or -dereferences NULL. - -Name both explicitly. attach_dev is the same as for a paging domain -- the -identity domain is just context 0. ---- - drivers/iommu/xen-iommu.c | 19 ++++++++++++++++++- - 1 file changed, 18 insertions(+), 1 deletion(-) - -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index 35cbc7d7026b..d43fd5a869a3 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -43,7 +43,24 @@ struct xen_iommu_domain { - static struct iommu_device xen_iommu_device; - static struct pv_iommu_capabilities caps; - --static struct xen_iommu_domain xen_iommu_identity_domain; -+static int xen_iommu_attach_dev(struct iommu_domain *domain, -+ struct device *dev); -+ -+static const struct iommu_domain_ops xen_iommu_identity_ops = { -+ .attach_dev = xen_iommu_attach_dev, -+}; -+ -+/* -+ * A statically allocated domain has to name its own type and ops; the core -+ * only fills those in for domains it allocated itself. Leaving type at 0 makes -+ * this an IOMMU_DOMAIN_BLOCKED domain with no ops at all. -+ */ -+static struct xen_iommu_domain xen_iommu_identity_domain = { -+ .domain = { -+ .type = IOMMU_DOMAIN_IDENTITY, -+ .ops = &xen_iommu_identity_ops, -+ }, -+}; - static unsigned long xen_iommu_pgsize_bitmap = XEN_PAGE_SIZE; - static bool map_single_pages = false; - --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0004-iommu-dma-Don-t-reserve-PCI-windows-under-a-paravirt.patch b/patches/pv-iommu-6.18/0004-iommu-dma-Don-t-reserve-PCI-windows-under-a-paravirt.patch deleted file mode 100644 index 51f1fd0a..00000000 --- a/patches/pv-iommu-6.18/0004-iommu-dma-Don-t-reserve-PCI-windows-under-a-paravirt.patch +++ /dev/null @@ -1,120 +0,0 @@ -From 4b057c93075246242d30b29b3f8d854f07382c9c Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Wed, 2 Sep 2026 10:51:23 -0700 -Subject: [PATCH 04/11] iommu/dma: Don't reserve PCI windows under a - paravirtual IOMMU - -A PCI host bridge's memory windows are reserved in IOVA space so that an -IOVA cannot collide with an address the bridge would route to MMIO rather -than to memory. Under a paravirtual IOMMU that reasoning does not apply: -the IOVA space belongs to the hypervisor, is programmed by hypercall, and -is not the bridge's address space at all. - -Worse, the windows a Xen guest sees are invented. pcifront hands out -iomem_resource itself as the root bus window, and a PVH guest has no host -bridge _CRS so Linux falls back to its catch-all default, clipped only by -the guest's physical address width: - - pci_bus 0000:00: root bus resource [mem 0x00000000-0x3fffffffffff] - -Reserving that swallows the whole aperture the hypervisor reported, and -every IOVA allocation fails -- even for a single page: - - nvidia 0000:00:01.0: IOVA alloc failed: len=1 limit=7fffffffff shift=12 - granule=4096 start_pfn=1 aperture=0-7fffffffff - NVRM: GPU0 osIovaMap: failed to map allocation (status = 0x59) - -The device then never gets a DMA mapping and the driver cannot attach. This -is inert without an IOMMU, since there is no IOVA space to reserve, so it -only appears once a guest drives one. - -Let the driver owning the IOVA space opt out. Kept to a private header -rather than a flag in struct iommu_ops: adding a field there changes the -CRC of every exported symbol whose type graph reaches it, which breaks -every already-built out-of-tree module. ---- - drivers/iommu/dma-iommu.c | 14 +++++++++++++- - drivers/iommu/iommu-priv.h | 9 +++++++++ - drivers/iommu/xen-iommu.c | 7 +++++++ - 3 files changed, 29 insertions(+), 1 deletion(-) - -diff --git a/drivers/iommu/dma-iommu.c b/drivers/iommu/dma-iommu.c -index b0dca7e7429a..3c2ad70d1c83 100644 ---- a/drivers/iommu/dma-iommu.c -+++ b/drivers/iommu/dma-iommu.c -@@ -28,6 +28,8 @@ - #include - #include - #include -+ -+#include "iommu-priv.h" - #include - #include - #include -@@ -558,7 +560,17 @@ static int iova_reserve_iommu_regions(struct device *dev, - LIST_HEAD(resv_regions); - int ret = 0; - -- if (dev_is_pci(dev)) { -+ /* -+ * A PCI host bridge's windows are reserved so that an IOVA cannot -+ * collide with an address the bridge would route to MMIO. Under a -+ * paravirtual IOMMU the IOVA space belongs to the hypervisor and is not -+ * the bridge's address space at all, and the windows a Xen guest sees -+ * are invented -- pcifront hands out iomem_resource itself, and a PVH -+ * guest with no host bridge _CRS gets Linux's catch-all default. There -+ * is nothing meaningful to reserve, and reserving it leaves no usable -+ * IOVA space whatsoever. -+ */ -+ if (dev_is_pci(dev) && !xen_iommu_manages_iova(dev)) { - ret = iova_reserve_pci_windows(to_pci_dev(dev), iovad); - if (ret) - return ret; -diff --git a/drivers/iommu/iommu-priv.h b/drivers/iommu/iommu-priv.h -index c95394cd03a7..292a73b1dd1d 100644 ---- a/drivers/iommu/iommu-priv.h -+++ b/drivers/iommu/iommu-priv.h -@@ -7,6 +7,15 @@ - #include - #include - -+#ifdef CONFIG_XEN_IOMMU -+bool xen_iommu_manages_iova(struct device *dev); -+#else -+static inline bool xen_iommu_manages_iova(struct device *dev) -+{ -+ return false; -+} -+#endif -+ - static inline const struct iommu_ops *dev_iommu_ops(struct device *dev) - { - /* -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index d43fd5a869a3..56b36160b378 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -13,6 +13,8 @@ - #include - #include - #include -+ -+#include "iommu-priv.h" - #include - #include - #include -@@ -372,6 +374,11 @@ static struct iommu_ops xen_iommu_ops = { - }, - }; - -+bool xen_iommu_manages_iova(struct device *dev) -+{ -+ return dev->iommu && dev_iommu_ops(dev) == &xen_iommu_ops; -+} -+ - static int __init xen_iommu_init(void) - { - long ret; --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0005-xen-pcifront-Let-callers-ask-for-a-device-s-machine-.patch b/patches/pv-iommu-6.18/0005-xen-pcifront-Let-callers-ask-for-a-device-s-machine-.patch deleted file mode 100644 index ac176902..00000000 --- a/patches/pv-iommu-6.18/0005-xen-pcifront-Let-callers-ask-for-a-device-s-machine-.patch +++ /dev/null @@ -1,195 +0,0 @@ -From 28f6cdec0a2ecb54354a6dc1daa0807618623b3a Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Wed, 2 Sep 2026 11:26:57 -0700 -Subject: [PATCH 05/11] xen/pcifront: Let callers ask for a device's machine - BDF - -The BDF a PV guest sees for a passed-through device is invented by pciback -in the hardware domain and never reaches the hypervisor, so it cannot be -used to name the device in a hypercall. PV-IOMMU needs to. - -pciback already publishes both halves of the mapping in its xenstore -directory -- dev-N is the machine BDF, vdev-N the one this domain sees -- -and pcifront already reads vdev-N when detaching, so the mapping is right -there. Walk it and hand the machine BDF back. - -Use it in the PV-IOMMU driver for both reattach and the reserved-region -query. With vPCI the SBDF a guest sees is one Xen assigned and can -translate, so this only changes what a PV guest sends. Both end up naming a -device the hypervisor can find. ---- - drivers/iommu/xen-iommu.c | 33 +++++++++++++++++---- - drivers/pci/xen-pcifront.c | 59 ++++++++++++++++++++++++++++++++++++++ - include/xen/pci.h | 14 +++++++++ - 3 files changed, 100 insertions(+), 6 deletions(-) - -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index 56b36160b378..7195bc76baa1 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -14,6 +14,8 @@ - #include - #include - -+#include -+ - #include "iommu-priv.h" - #include - #include -@@ -133,6 +135,29 @@ static struct iommu_domain *xen_iommu_domain_alloc_paging(struct device *dev) - return &domain->domain; - } - -+/* -+ * Name a device the way the hypervisor knows it. With vPCI the SBDF this domain -+ * sees is one Xen assigned and can translate. A PV guest's comes from pciback -+ * instead and means nothing to Xen, so use the machine BDF pciback published -+ * alongside it. -+ */ -+static void xen_iommu_set_dev(struct physdev_pci_device *out, -+ struct pci_dev *pdev) -+{ -+ u32 sbdf; -+ -+ if (!pcifront_machine_sbdf(pdev, &sbdf)) { -+ out->seg = sbdf >> 16; -+ out->bus = (sbdf >> 8) & 0xff; -+ out->devfn = sbdf & 0xff; -+ return; -+ } -+ -+ out->seg = pci_domain_nr(pdev->bus); -+ out->bus = pdev->bus->number; -+ out->devfn = pdev->devfn; -+} -+ - static struct iommu_device *xen_iommu_probe_device(struct device *dev) - { - if (!dev_is_pci(dev)) -@@ -240,9 +265,7 @@ static int xen_iommu_attach_dev(struct iommu_domain *domain, struct device *dev) - - pdev = to_pci_dev(dev); - -- reattach.dev.seg = pci_domain_nr(pdev->bus); -- reattach.dev.bus = pdev->bus->number; -- reattach.dev.devfn = pdev->devfn; -+ xen_iommu_set_dev(&reattach.dev, pdev); - - return HYPERVISOR_iommu_op(IOMMU_reattach_device, &reattach); - } -@@ -303,9 +326,7 @@ static void xen_iommu_get_resv_regions(struct device *dev, struct list_head *hea - map.nr_entries = 0; - map.flags = 0; - -- map.dev.pci.seg = pci_domain_nr(pdev->bus); -- map.dev.pci.bus = pdev->bus->number; -- map.dev.pci.devfn = pdev->devfn; -+ xen_iommu_set_dev(&map.dev.pci, pdev); - - ret = HYPERVISOR_memory_op(XENMEM_reserved_device_memory_map, &map); - -diff --git a/drivers/pci/xen-pcifront.c b/drivers/pci/xen-pcifront.c -index 11636634ae51..2654ce573c01 100644 ---- a/drivers/pci/xen-pcifront.c -+++ b/drivers/pci/xen-pcifront.c -@@ -8,6 +8,7 @@ - #include - #include - #include -+#include - #include - #include - #include -@@ -404,6 +405,64 @@ static int pcifront_claim_resource(struct pci_dev *dev, void *data) - return 0; - } - -+/* -+ * pciback names every assigned device twice in its xenstore directory: dev-N is -+ * the machine BDF and vdev-N is the BDF this domain sees. Only the former means -+ * anything to the hypervisor, so hand it back to callers that have to name a -+ * device to Xen -- the PV-IOMMU driver -- since the BDF a PV guest sees was -+ * invented in the backend and never reached Xen at all. -+ * -+ * Returns the machine SBDF packed as Xen expects it: seg << 16 | bus << 8 | -+ * devfn. -+ */ -+int pcifront_machine_sbdf(const struct pci_dev *dev, u32 *sbdf) -+{ -+ unsigned int domain, bus, slot, func; -+ struct pcifront_device *pdev; -+ int i, num_devs; -+ char str[64]; -+ -+ if (dev->bus->ops != &pcifront_bus_ops) -+ return -ENODEV; -+ -+ pdev = pcifront_get_pdev(dev->bus->sysdata); -+ if (!pdev) -+ return -ENODEV; -+ -+ if (xenbus_scanf(XBT_NIL, pdev->xdev->otherend, "num_devs", "%d", -+ &num_devs) != 1) -+ return -ENODEV; -+ -+ for (i = 0; i < num_devs; i++) { -+ if (snprintf(str, sizeof(str), "vdev-%d", i) >= sizeof(str) - 1) -+ return -ENODEV; -+ -+ if (xenbus_scanf(XBT_NIL, pdev->xdev->otherend, str, -+ "%x:%x:%x.%x", &domain, &bus, &slot, -+ &func) != 4) -+ continue; -+ -+ if (domain != pci_domain_nr(dev->bus) || -+ bus != dev->bus->number || -+ PCI_DEVFN(slot, func) != dev->devfn) -+ continue; -+ -+ if (snprintf(str, sizeof(str), "dev-%d", i) >= sizeof(str) - 1) -+ return -ENODEV; -+ -+ if (xenbus_scanf(XBT_NIL, pdev->xdev->otherend, str, -+ "%x:%x:%x.%x", &domain, &bus, &slot, -+ &func) != 4) -+ return -ENODEV; -+ -+ *sbdf = (domain << 16) | (bus << 8) | PCI_DEVFN(slot, func); -+ return 0; -+ } -+ -+ return -ENODEV; -+} -+EXPORT_SYMBOL_GPL(pcifront_machine_sbdf); -+ - static int pcifront_scan_bus(struct pcifront_device *pdev, - unsigned int domain, unsigned int bus, - struct pci_bus *b) -diff --git a/include/xen/pci.h b/include/xen/pci.h -index 424b8ea89ca8..b5efff694a03 100644 ---- a/include/xen/pci.h -+++ b/include/xen/pci.h -@@ -3,6 +3,20 @@ - #ifndef __XEN_PCI_H__ - #define __XEN_PCI_H__ - -+#include -+#include -+ -+struct pci_dev; -+ -+#if defined(CONFIG_XEN_PCIDEV_FRONTEND) -+int pcifront_machine_sbdf(const struct pci_dev *dev, u32 *sbdf); -+#else -+static inline int pcifront_machine_sbdf(const struct pci_dev *dev, u32 *sbdf) -+{ -+ return -ENODEV; -+} -+#endif -+ - #if defined(CONFIG_XEN_DOM0) - int xen_reset_device(const struct pci_dev *dev); - int xen_find_device_domain_owner(struct pci_dev *dev); --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0006-iommu-xen-Default-a-PV-guest-to-the-identity-context.patch b/patches/pv-iommu-6.18/0006-iommu-xen-Default-a-PV-guest-to-the-identity-context.patch deleted file mode 100644 index 4a44fe5f..00000000 --- a/patches/pv-iommu-6.18/0006-iommu-xen-Default-a-PV-guest-to-the-identity-context.patch +++ /dev/null @@ -1,69 +0,0 @@ -From b5d8cdfb20894156c9da6642adfed05d371e9926 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Wed, 2 Sep 2026 12:02:36 -0700 -Subject: [PATCH 06/11] iommu/xen: Default a PV guest to the identity context - -A PV guest reaches its devices through xen-swiotlb, which hands them -machine addresses. Attaching one to a translated IOMMU context as well -leaves two translations disagreeing about every address, and the DMA layer -rejects the combination outright: - - WARNING: CPU: 0 PID: 11 at kernel/dma/mapping.c:881 dma_supported+0x5e/0x80 - dma_set_mask+0x24/0xe0 - nv_set_dma_address_size+0x52/0x70 [nvidia] - nv_pci_probe+0x28f/0x1090 [nvidia] - -dma_supported() warns and returns false when a device both uses iommu-dma -and has dma_map_ops, which in a PV guest it always does. The driver's -dma_set_mask() then fails and every mapping after it is wrong; the GPU here -got as far as GspStatusQueueInit before returning NV_ERR_RESET_REQUIRED. - -Report IOMMU_DOMAIN_IDENTITY as the default domain type there, which is -context 0 and what the hypervisor already advertises as identity. Normal -DMA stays on xen-swiotlb and translated contexts can still be asked for -explicitly, which is what a guest wanting one for VFIO would do. - -Guests whose devices come through vPCI are unaffected; they have no -dma_map_ops of their own and use iommu-dma as before. ---- - drivers/iommu/xen-iommu.c | 17 +++++++++++++++++ - 1 file changed, 17 insertions(+) - -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index 7195bc76baa1..f06dc957e8b0 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -158,6 +158,22 @@ static void xen_iommu_set_dev(struct physdev_pci_device *out, - out->devfn = pdev->devfn; - } - -+/* -+ * A PV guest reaches its devices through xen-swiotlb, which hands them machine -+ * addresses. Putting a device in a translated context as well leaves two -+ * translations disagreeing about every address, and the DMA layer refuses the -+ * combination outright -- dma_supported() warns and fails once a device both -+ * uses iommu-dma and has dma_map_ops, which every PV guest does. Default to -+ * the identity context there and let translated ones be asked for explicitly. -+ */ -+static int xen_iommu_def_domain_type(struct device *dev) -+{ -+ if (xen_pv_domain()) -+ return IOMMU_DOMAIN_IDENTITY; -+ -+ return 0; -+} -+ - static struct iommu_device *xen_iommu_probe_device(struct device *dev) - { - if (!dev_is_pci(dev)) -@@ -386,6 +402,7 @@ static struct iommu_ops xen_iommu_ops = { - .probe_device = xen_iommu_probe_device, - .device_group = pci_device_group, - .get_resv_regions = xen_iommu_get_resv_regions, -+ .def_domain_type = xen_iommu_def_domain_type, - .default_domain_ops = &(const struct iommu_domain_ops) { - .map_pages = xen_iommu_map_pages, - .unmap_pages = xen_iommu_unmap_pages, --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0007-PCI-MSI-don-t-write-the-MSI-X-table-in-a-Xen-PV-gues.patch b/patches/pv-iommu-6.18/0007-PCI-MSI-don-t-write-the-MSI-X-table-in-a-Xen-PV-gues.patch deleted file mode 100644 index c7871770..00000000 --- a/patches/pv-iommu-6.18/0007-PCI-MSI-don-t-write-the-MSI-X-table-in-a-Xen-PV-gues.patch +++ /dev/null @@ -1,56 +0,0 @@ -From 10aaa708318c723f223a7223c12ff61782d74ec9 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Wed, 2 Sep 2026 20:52:03 -0700 -Subject: [PATCH 07/11] PCI/MSI: don't write the MSI-X table in a Xen PV guest - -A PV guest does not own the MSI-X table of a passed-through device. Xen -programs it when mapping the pirq and maps the table read-only, so the -mask, message and unmask writes in pci_write_msg_msix() fault. - -vfio-pci refreshes the cached message unconditionally before enabling a -vector, to undo a backdoor reset, which crashes a PV guest: - - BUG: unable to handle page fault for address: ffffc9004001d000 - #PF: supervisor write access in kernel mode - RIP: __pci_write_msi_msg+0x74/0x1f0 - vfio_msi_set_vector_signal+0x2c9/0x320 - -Return early as the is_virtual case already does; nothing in a PV guest -programs the table itself, the Xen MSI domain routes allocation through -PHYSDEVOP_map_pirq and Xen does the write. ---- - drivers/pci/msi/msi.c | 11 +++++++++++ - 1 file changed, 11 insertions(+) - -diff --git a/drivers/pci/msi/msi.c b/drivers/pci/msi/msi.c -index 7e2332869306..c01f44195817 100644 ---- a/drivers/pci/msi/msi.c -+++ b/drivers/pci/msi/msi.c -@@ -12,6 +12,8 @@ - #include - #include - -+#include -+ - #include "../pci.h" - #include "msi.h" - -@@ -214,6 +216,15 @@ static inline void pci_write_msg_msix(struct msi_desc *desc, struct msi_msg *msg - - if (desc->pci.msi_attrib.is_virtual) - return; -+ -+ /* -+ * A PV guest never owns the MSI-X table. Xen programs it when mapping -+ * the pirq and maps it read-only, so the masking and message writes -+ * below would fault; vfio-pci refreshes the message unconditionally. -+ */ -+ if (xen_pv_domain()) -+ return; -+ - /* - * The specification mandates that the entry is masked - * when the message is modified: --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0008-iommu-xen-batch-map-subops-over-contiguous-runs.patch b/patches/pv-iommu-6.18/0008-iommu-xen-batch-map-subops-over-contiguous-runs.patch deleted file mode 100644 index 5264ab4e..00000000 --- a/patches/pv-iommu-6.18/0008-iommu-xen-batch-map-subops-over-contiguous-runs.patch +++ /dev/null @@ -1,104 +0,0 @@ -From 50851dec86216ccead289dcc470674cea152d6d9 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Thu, 3 Sep 2026 01:00:50 -0700 -Subject: [PATCH 08/11] iommu/xen: batch map subops over contiguous runs - -A PV domain took the single-page path unconditionally, one hypercall per -4K page, because pfn_to_gfn(x + 1) != pfn_to_gfn(x) + 1 there in general -and the batched path assumed the whole request was contiguous. Every -subop also carries an IOTLB flush on the hypervisor side, so a large -mapping costs a flush per page: a 4G premapping is a million hypercalls -and a million flushes, which no caller waits out. - -Walk the request and issue one subop per maximal run contiguous in both -dfn and gfn, as the TODO describing this asked for. Behaviour is -unchanged where pfn_to_gfn is the identity, the run then covering the -whole request. map_single_pages remains as a module parameter. ---- - drivers/iommu/xen-iommu.c | 48 +++++++++++++++++++++++---------------- - 1 file changed, 29 insertions(+), 19 deletions(-) - -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index f06dc957e8b0..3a9bf55ecadb 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -67,6 +67,9 @@ static struct xen_iommu_domain xen_iommu_identity_domain = { - }; - static unsigned long xen_iommu_pgsize_bitmap = XEN_PAGE_SIZE; - static bool map_single_pages = false; -+module_param(map_single_pages, bool, 0444); -+MODULE_PARM_DESC(map_single_pages, -+ "Issue one hypercall per page instead of per contiguous run"); - - static inline struct xen_iommu_domain *to_xen_iommu_domain(struct iommu_domain *dom) - { -@@ -221,7 +224,6 @@ static int xen_iommu_map_pages(struct iommu_domain *domain, unsigned long iova, - - if (map_single_pages) { - size_t i = 0; -- map.nr_pages = 1; - - for (; i < pgcount; i++) { - map.gfn = pfn_to_gfn(pfn0 + i); -@@ -234,11 +236,33 @@ static int xen_iommu_map_pages(struct iommu_domain *domain, unsigned long iova, - break; - } - } else { -- map.nr_pages = pgcount; -- map.gfn = pfn_to_gfn(pfn0); -- map.dfn = dfn0; -+ size_t done = 0; - -- ret = HYPERVISOR_iommu_op(IOMMU_map_pages, &map); -+ /* -+ * A subop covers a run of pages contiguous in both dfn and gfn. -+ * In a PV domain pfn_to_gfn(x + 1) != pfn_to_gfn(x) + 1 in -+ * general, so walk the request and issue one subop per maximal -+ * run rather than assuming the whole of it is contiguous. -+ */ -+ while (done < pgcount) { -+ uint64_t gfn0 = pfn_to_gfn(pfn0 + done); -+ size_t run = 1; -+ -+ while (done + run < pgcount && -+ pfn_to_gfn(pfn0 + done + run) == gfn0 + run) -+ run++; -+ -+ map.gfn = gfn0; -+ map.dfn = dfn0 + done; -+ map.nr_pages = run; -+ -+ ret = HYPERVISOR_iommu_op(IOMMU_map_pages, &map); -+ -+ if (ret) -+ break; -+ -+ done += run; -+ } - } - - if (mapped) -@@ -475,20 +499,6 @@ static int __init xen_iommu_init(void) - - xen_iommu_pgsize_bitmap = caps.pgsize_mask; - -- if (xen_domain_type == XEN_PV_DOMAIN) -- /* TODO: In PV domain, due to the existing pfn-gfn mapping we need to -- * consider that under certains circonstances, we have : -- * pfn_to_gfn(x + 1) != pfn_to_gfn(x) + 1 -- * -- * In these cases, we would want to separate the subop into several calls. -- * (only doing the grouped operation when the mapping is actually contigous) -- * Only map operation would be affected, as unmap actually uses dfn which -- * doesn't have this kind of mapping. -- * -- * Force single-page operations to work arround this issue for now. -- */ -- map_single_pages = true; -- - /* Initialize identity domain */ - xen_iommu_identity_domain.ctx_no = 0; - --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0009-iommu-xen-do-not-claim-the-IOMMU-itself.patch b/patches/pv-iommu-6.18/0009-iommu-xen-do-not-claim-the-IOMMU-itself.patch deleted file mode 100644 index e418aaa9..00000000 --- a/patches/pv-iommu-6.18/0009-iommu-xen-do-not-claim-the-IOMMU-itself.patch +++ /dev/null @@ -1,53 +0,0 @@ -From 17bc7d257503a52576e685b5b57369efd4009b5b Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Thu, 3 Sep 2026 09:15:38 -0700 -Subject: [PATCH 09/11] iommu/xen: do not claim the IOMMU itself - -An IOMMU does not sit behind itself, so the hypervisor has no device for -it and reattach fails with -ENODEV. The IOMMU core does not allow a -driver to fail the first domain attach, so claiming it takes down -registration of the whole driver: - - WARNING at drivers/iommu/iommu.c:3037 iommu_setup_default_domain - iommu_device_register - xen_iommu_init - xen-iommu: Unable to register Xen IOMMU device -19 - -Only reachable in the initial domain, which sees the real topology; a -guest is never given the IOMMU. Skip PCI class 0806. ---- - drivers/iommu/xen-iommu.c | 11 +++++++++++ - 1 file changed, 11 insertions(+) - -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index 3a9bf55ecadb..78ced18c4c90 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -65,6 +65,9 @@ static struct xen_iommu_domain xen_iommu_identity_domain = { - .ops = &xen_iommu_identity_ops, - }, - }; -+/* PCI base class 08h, sub-class 06h: IOMMU. */ -+#define XEN_IOMMU_PCI_CLASS 0x0806 -+ - static unsigned long xen_iommu_pgsize_bitmap = XEN_PAGE_SIZE; - static bool map_single_pages = false; - module_param(map_single_pages, bool, 0444); -@@ -192,6 +195,14 @@ static struct iommu_device *xen_iommu_probe_device(struct device *dev) - to_pci_dev(dev)->hdr_type != PCI_HEADER_TYPE_NORMAL) - return ERR_PTR(-ENODEV); - -+ /* -+ * An IOMMU does not sit behind itself, so Xen has no device for it and -+ * a context can never be attached. Claiming it fails the first attach, -+ * which the IOMMU core treats as fatal to registering the driver. -+ */ -+ if ((to_pci_dev(dev)->class >> 8) == XEN_IOMMU_PCI_CLASS) -+ return ERR_PTR(-ENODEV); -+ - return &xen_iommu_device; - } - --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0010-x86-xen-disable-the-right-interrupt-type-when-tearin.patch b/patches/pv-iommu-6.18/0010-x86-xen-disable-the-right-interrupt-type-when-tearin.patch deleted file mode 100644 index e0fcbf2b..00000000 --- a/patches/pv-iommu-6.18/0010-x86-xen-disable-the-right-interrupt-type-when-tearin.patch +++ /dev/null @@ -1,55 +0,0 @@ -From 57110ab30a67bb0ed5b780b69f4d04e641ad763d Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Thu, 3 Sep 2026 10:53:59 -0700 -Subject: [PATCH 10/11] x86/xen: disable the right interrupt type when tearing - down MSI - -pci_disable_msix() clears msix_enabled in pci_msix_shutdown() before -pci_free_msi_irqs() reaches the domain teardown, so by the time -xen_pv_teardown_msi_irqs() runs the flag can no longer say which of the -two the device was using. It always took the MSI branch. - -The backend therefore never saw XEN_PCI_OP_disable_msix and left MSI-X -enabled on the real device, and a later guest asking to enable it got --EALREADY from xen_pcibk_enable_msix(). Seen as a device usable exactly -once per assignment. - -Take the type from a descriptor instead. The descriptor lock is already -held by the caller. ---- - arch/x86/pci/xen.c | 19 ++++++++++++++++++- - 1 file changed, 18 insertions(+), 1 deletion(-) - -diff --git a/arch/x86/pci/xen.c b/arch/x86/pci/xen.c -index b8755cde2419..de9a1c946a3f 100644 ---- a/arch/x86/pci/xen.c -+++ b/arch/x86/pci/xen.c -@@ -400,7 +400,24 @@ static void xen_teardown_msi_irqs(struct pci_dev *dev) - - static void xen_pv_teardown_msi_irqs(struct pci_dev *dev) - { -- if (dev->msix_enabled) -+ struct msi_desc *desc; -+ bool msix = false; -+ -+ /* -+ * pci_disable_msix() clears msix_enabled in pci_msix_shutdown() before -+ * freeing the irqs brings us here, so it cannot say which of the two -+ * the device was using. Ask a descriptor instead: getting this wrong -+ * leaves the backend with MSI-X still enabled on the real device, and -+ * the next attempt to enable it fails with -EALREADY. -+ * -+ * The descriptor lock is already held by pci_disable_msix(). -+ */ -+ msi_for_each_desc(desc, &dev->dev, MSI_DESC_ALL) { -+ msix = desc->pci.msi_attrib.is_msix; -+ break; -+ } -+ -+ if (msix) - xen_pci_frontend_disable_msix(dev); - else - xen_pci_frontend_disable_msi(dev); --- -2.55.0 - diff --git a/patches/pv-iommu-6.18/0011-iommu-xen-flush-the-IOTLB-once-per-mapping-not-once-.patch b/patches/pv-iommu-6.18/0011-iommu-xen-flush-the-IOTLB-once-per-mapping-not-once-.patch deleted file mode 100644 index 8ab367cb..00000000 --- a/patches/pv-iommu-6.18/0011-iommu-xen-flush-the-IOTLB-once-per-mapping-not-once-.patch +++ /dev/null @@ -1,159 +0,0 @@ -From 8c4bb578f2c5d44403928bc93f36f13d14c7d1e9 Mon Sep 17 00:00:00 2001 -From: Alex Zenla -Date: Thu, 3 Sep 2026 12:21:19 -0700 -Subject: [PATCH 11/11] iommu/xen: flush the IOTLB once per mapping, not once - per subop - -A map subop covers a run of frames contiguous in both dfn and gfn, and -in a PV domain gfns are machine frames, so a scattered buffer breaks -into runs of a few pages. Each subop then flushed, and against an -emulated IOMMU a flush is a command and a completion wait that costs far -more than the mapping does. Mapping several gigabytes took long enough -for callers to give up. - -The core already has somewhere to put this: ask the hypervisor to skip -the per-subop flush and issue one over the whole range from -iotlb_sync_map(). Gated on IOMMUCAP_deferred_flush so an older -hypervisor keeps flushing per subop rather than never flushing at all. ---- - drivers/iommu/xen-iommu.c | 31 +++++++++++++++++++++++++++++++ - include/xen/interface/pv-iommu.h | 32 ++++++++++++++++++++++++++++++++ - 2 files changed, 63 insertions(+) - -diff --git a/drivers/iommu/xen-iommu.c b/drivers/iommu/xen-iommu.c -index 78ced18c4c90..36ad0dbee42c 100644 ---- a/drivers/iommu/xen-iommu.c -+++ b/drivers/iommu/xen-iommu.c -@@ -69,6 +69,8 @@ static struct xen_iommu_domain xen_iommu_identity_domain = { - #define XEN_IOMMU_PCI_CLASS 0x0806 - - static unsigned long xen_iommu_pgsize_bitmap = XEN_PAGE_SIZE; -+static bool deferred_flush; -+ - static bool map_single_pages = false; - module_param(map_single_pages, bool, 0444); - MODULE_PARM_DESC(map_single_pages, -@@ -233,6 +235,14 @@ static int xen_iommu_map_pages(struct iommu_domain *domain, unsigned long iova, - if (prot & IOMMU_CACHE) - map.map_flags |= IOMMU_MAP_cache; - -+ /* -+ * A scattered buffer costs one subop per run of contiguous frames, and -+ * each one flushes. Leave the flush to iotlb_sync_map(), which pays it -+ * once for the whole mapping. -+ */ -+ if (deferred_flush) -+ map.map_flags |= IOMMU_MAP_no_flush; -+ - if (map_single_pages) { - size_t i = 0; - -@@ -285,6 +295,23 @@ static int xen_iommu_map_pages(struct iommu_domain *domain, unsigned long iova, - return ret; - } - -+static int xen_iommu_sync_map(struct iommu_domain *domain, unsigned long iova, -+ size_t size) -+{ -+ struct xen_iommu_domain *dom = to_xen_iommu_domain(domain); -+ struct pv_iommu_flush_pages flush = { -+ .ctx_no = dom->ctx_no, -+ .dfn = addr_to_pfn(iova), -+ .pgsize = PAGE_SIZE, -+ .nr_pages = size >> PAGE_SHIFT, -+ }; -+ -+ if (!deferred_flush) -+ return 0; -+ -+ return HYPERVISOR_iommu_op(IOMMU_flush_pages, &flush); -+} -+ - static size_t xen_iommu_unmap_pages(struct iommu_domain *domain, unsigned long iova, - size_t pgsize, size_t pgcount, - struct iommu_iotlb_gather *iotlb_gather) -@@ -440,6 +467,7 @@ static struct iommu_ops xen_iommu_ops = { - .def_domain_type = xen_iommu_def_domain_type, - .default_domain_ops = &(const struct iommu_domain_ops) { - .map_pages = xen_iommu_map_pages, -+ .iotlb_sync_map = xen_iommu_sync_map, - .unmap_pages = xen_iommu_unmap_pages, - .attach_dev = xen_iommu_attach_dev, - .iova_to_phys = xen_iommu_iova_to_phys, -@@ -503,6 +531,9 @@ static int __init xen_iommu_init(void) - pr_info("default_identity=%c\n", (caps.cap_flags & IOMMUCAP_default_identity) ? 'y' : 'n'); - pr_info("cache=%c\n", (caps.cap_flags & IOMMUCAP_cache) ? 'y' : 'n'); - -+ deferred_flush = caps.cap_flags & IOMMUCAP_deferred_flush; -+ pr_info("deferred_flush=%c\n", deferred_flush ? 'y' : 'n'); -+ - if (caps.max_ctx_no == 0) { - pr_err("Unable to use IOMMU PV driver (no context available ?)\n"); - return -ENOTSUPP; /* Unable to use IOMMU PV ? */ -diff --git a/include/xen/interface/pv-iommu.h b/include/xen/interface/pv-iommu.h -index a4a470319486..d4e58edd6e0d 100644 ---- a/include/xen/interface/pv-iommu.h -+++ b/include/xen/interface/pv-iommu.h -@@ -36,6 +36,7 @@ enum { - IOMMU_flush_nested, /* if IOMMUCAP_nested */ - IOMMU_attach_pasid, /* if IOMMUCAP_pasid */ - IOMMU_detach_pasid, /* if IOMMUCAP_pasid */ -+ IOMMU_flush_pages, /* if IOMMUCAP_deferred_flush */ - }; - - /** -@@ -65,6 +66,11 @@ enum { - */ - #define IOMMUCAP_identity (1 << 4) - -+/** -+ * Support for IOMMU_MAP_no_flush and IOMMU_flush_pages. -+ */ -+#define IOMMUCAP_deferred_flush (1 << 5) -+ - /** - * IOMMU_query_capabilities - * Query PV-IOMMU capabilities for this domain. -@@ -163,6 +169,12 @@ DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_free_t); - /* Enforce DMA coherency */ - #define IOMMU_MAP_cache (1 << 2) - -+/* -+ * Don't flush the IOTLB for this call. The caller must issue IOMMU_flush_pages -+ * over the range before a device uses it. -+ */ -+#define IOMMU_MAP_no_flush (1 << 3) -+ - /** - * IOMMU_map_pages - * Map pages on a IOMMU context. -@@ -193,6 +205,26 @@ struct pv_iommu_map_pages { - /* OUT: Number of pages actually mapped */ - uint32_t mapped; - }; -+ -+/** -+ * IOMMU_flush_pages -+ * Flush the IOTLB over a device frame range on a IOMMU context. -+ * -+ * Pairs with IOMMU_MAP_no_flush. -+ */ -+struct pv_iommu_flush_pages { -+ /* IN: IOMMU context number */ -+ uint16_t ctx_no; -+ -+ /* IN: Device frame number */ -+ uint64_aligned_t dfn; -+ -+ /* IN: Size of pages to flush */ -+ uint32_t pgsize; -+ -+ /* IN: Number of pages to flush */ -+ uint32_t nr_pages; -+}; - typedef struct pv_iommu_map_pages pv_iommu_map_pages_t; - DEFINE_GUEST_HANDLE_STRUCT(pv_iommu_map_pages_t); - --- -2.55.0 -