Linux Documentation
 help / color / mirror / Atom feed
* Re: [PATCH v23 12/14] crypto: qce - Map crypto memory for DMA
From: Mukesh Savaliya @ 2026-07-23  9:20 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: dmaengine, linux-doc, linux-kernel, linux-arm-msm, linux-crypto,
	linux-arm-kernel, Bartosz Golaszewski, Bartosz Golaszewski,
	Vinod Koul, Jonathan Corbet, Thara Gopinath, Herbert Xu,
	David S. Miller, Udit Tiwari, Md Sadre Alam, Dmitry Baryshkov,
	Manivannan Sadhasivam, Stephan Gerhold, Bjorn Andersson,
	Peter Ujfalusi, Michal Simek, Frank Li, Andy Gross,
	Neil Armstrong, Vignesh Raghavendra
In-Reply-To: <CAMRc=MdLMQgJsxAMXLBkZNWUiz9fYh0j=C5ErbFoDLV=VfPUzg@mail.gmail.com>



On 7/22/2026 2:47 PM, Bartosz Golaszewski wrote:
> On Tue, 21 Jul 2026 20:20:05 +0200, Mukesh Savaliya
> <mukesh.savaliya@oss.qualcomm.com> said:
>>
>>
>> On 7/21/2026 7:03 PM, Bartosz Golaszewski wrote:
>>> From: Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
>>
>> [...]
>>
>>>    static int qce_crypto_probe(struct platform_device *pdev)
>>>    {
>>>    	struct device *dev = &pdev->dev;
>>>    	struct qce_device *qce;
>>> +	struct resource *res;
>>>    	int ret;
>>>
>>>    	qce = devm_kzalloc(dev, sizeof(*qce), GFP_KERNEL);
>>> @@ -204,7 +213,7 @@ static int qce_crypto_probe(struct platform_device *pdev)
>>>    	qce->dev = dev;
>>>    	platform_set_drvdata(pdev, qce);
>>>
>>> -	qce->base = devm_platform_ioremap_resource(pdev, 0);
>>> +	qce->base = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
>>>    	if (IS_ERR(qce->base))
>> Would you like to add any debug/error log ?
>>>    		return PTR_ERR(qce->base);
>>>
>>> @@ -254,6 +263,18 @@ static int qce_crypto_probe(struct platform_device *pdev)
>>>    	qce->async_req_enqueue = qce_async_request_enqueue;
>>>    	qce->async_req_done = qce_async_request_done;
>>>
>>> +	qce->dma_size = resource_size(res);
>>> +	qce->base_dma = dma_map_resource(dev, res->start, qce->dma_size,
>>> +					 DMA_BIDIRECTIONAL, 0);
>>> +	qce->base_phys = res->start;
>>> +	ret = dma_mapping_error(dev, qce->base_dma);
>>> +	if (ret)
>> Would you like to add any debug/error log ?
> 
> No, I don't think this is needed and at v23 this really is useless bikeshedding
> and doesn't add anything to the discussion and only delays getting
> this upstream.
> 
Okay, got it. Thanks !

> Bartosz
> 
>>> +		return ret;
>>> +
>>> +	ret = devm_add_action_or_reset(qce->dev, qce_crypto_unmap_dma, qce);
>>> +	if (ret)
>> same here
>>> +		return ret;
>>> +
>>>    	return devm_qce_register_algs(qce);
>>>    }
>>>
>>
>> [...]
>>
>>


^ permalink raw reply

* Re: [PATCH v23 06/14] dmaengine: qcom: bam_dma: add support for BAM locking
From: Stephan Gerhold @ 2026-07-23 10:04 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Vinod Koul, Jonathan Corbet, Thara Gopinath, Herbert Xu,
	David S. Miller, Udit Tiwari, Md Sadre Alam, Dmitry Baryshkov,
	Manivannan Sadhasivam, Bjorn Andersson, Mukesh Kumar Savaliya,
	Peter Ujfalusi, Michal Simek, Frank Li, Neil Armstrong,
	Vignesh Raghavendra, dmaengine, linux-doc, linux-kernel,
	linux-arm-msm, linux-crypto, linux-arm-kernel, sashiko-reviews,
	Bartosz Golaszewski
In-Reply-To: <amDuqoRBg5qGafi2@linaro.org>

On Wed, Jul 22, 2026 at 06:24:10PM +0200, Stephan Gerhold wrote:
> On Wed, Jul 22, 2026 at 08:37:19AM -0700, Bartosz Golaszewski wrote:
> > On Wed, 22 Jul 2026 16:20:52 +0200, Stephan Gerhold
> > <stephan.gerhold@linaro.org> said:
> > > On Wed, Jul 22, 2026 at 07:11:10AM -0700, Bartosz Golaszewski wrote:
> > >> On Wed, 22 Jul 2026 14:47:56 +0200, Stephan Gerhold
> > >> <stephan.gerhold@linaro.org> said:
> > >> > On Wed, Jul 22, 2026 at 02:34:52AM -0700, Bartosz Golaszewski wrote:
> > >> >> On Wed, 22 Jul 2026 10:59:09 +0200, Stephan Gerhold
> > >> >> <stephan.gerhold@linaro.org> said:
> > >> >> > On Tue, Jul 21, 2026 at 01:48:52PM +0000, sashiko-bot@kernel.org wrote:
> > >> >> >> Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
> > >> >> >> - [High] State leak causing hardware misconfiguration on channel reuse due to uncleared `lock_scratchpad_addr` and hardcoded direction.
> > >> >> >> - [High] Channel state corruption when `bam_slave_config` fails validation.
> > >> >> >> - [High] Infinite BH workqueue loop and unratelimited log flood on memory allocation failure.
> > >> >> >> - [High] DMA API violation: `lock_ce` buffer shares a cacheline with frequently CPU-modified struct members.
> > >> >> >> - [Low] Unused assignment of `hdesc` in IRQ handler.
> > >> >> >> --
> > >> >> >>
> > >> >> >> --- Patch [6]: [PATCH v23 06/14] dmaengine: qcom: bam_dma: add support for BAM locking ---
> > >> >> >> commit 8b44c5e4935cbb61832adb3262df00b3b606a283
> > >> >> >> Author: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
> > >> >> >>
> > >> >> >> dmaengine: qcom: bam_dma: add support for BAM locking
> > >> >> >>
> > >> >> >> This commit adds support for BAM pipe locking by prepending and appending
> > >> >> >> dummy command descriptors with LOCK/UNLOCK bits when starting DMA on a TX
> > >> >> >> channel. It uses a peripheral-local scratchpad address communicated via slave
> > >> >> >> config for the dummy writes.
> > >> >> >>
> > >> >> >> > diff --git a/drivers/dma/qcom/bam_dma.c b/drivers/dma/qcom/bam_dma.c
> > >> >> >> > index f3e713a5259c2..411c60c4433e2 100644
> > >> >> >> > --- a/drivers/dma/qcom/bam_dma.c
> > >> >> >> > +++ b/drivers/dma/qcom/bam_dma.c
> > >> >> [...]
> > >> >> >> [ ... ]
> > >> >> >> > @@ -1064,6 +1197,7 @@ static void bam_start_dma(struct bam_chan *bchan)
> > >> >> >> >
> > >> >> >> >  	lockdep_assert_held(&bchan->vc.lock);
> > >> >> >> >
> > >> >> >> > +	vd = vchan_next_desc(&bchan->vc);
> > >> >> >> >  	if (!vd)
> > >> >> >> >  		return;
> > >> >> >> >
> > >> >> >> > @@ -1072,6 +1206,24 @@ static void bam_start_dma(struct bam_chan *bchan)
> > >> >> >> >  		return;
> > >> >> >> >
> > >> >> >> >  	while (vd && !IS_BUSY(bchan)) {
> > >> >> >> > +		/*
> > >> >> >> > +		 * Open a LOCK/UNLOCK bracket around each fresh sequence.
> > >> >> >> > +		 * Sentinels inserted by bam_setup_pipe_lock() are skipped: they
> > >> >> >> > +		 * already have bam_locked set and must not trigger a second pair.
> > >> >> >> > +		 */
> > >> >> >> > +		if (!bchan->bam_locked) {
> > >> >> >> > +			ret = bam_setup_pipe_lock(bchan);
> > >> >> >> > +			if (ret) {
> > >> >> >> > +				dev_err_ratelimited(bdev->dev,
> > >> >> >> > +					"failed to setup the pipe lock, deferring transfer: %d\n",
> > >> >> >> > +					ret);
> > >> >> >> > +				queue_work(system_bh_highpri_wq, &bdev->work);
> > >> >> >> > +				break;
> > >> >> >> > +			}
> > >> >> >>
> > >> >> >> [Severity: High]
> > >> >> >> Does requeuing to a high-priority BH workqueue upon ENOMEM cause CPU
> > >> >> >> starvation? Because system_bh_highpri_wq executes in softirq context,
> > >> >> >> requeuing immediately without sleeping prevents the memory reclaim needed
> > >> >> >> to resolve the GFP_NOWAIT failure, leading to an infinite spin loop.
> > >> >> >>
> > >> >> >
> > >> >> > It looks like my suggestion to queue_work() again in the error path of
> > >> >> > bam_start_dma() wasn't great if we end up causing an infinite spin loop.
> > >> >> > We do need to retry somehow or report an error though, I don't think
> > >> >> > aborting and leaving the desccriptors completely unhandled is an option
> > >> >> > either... :(
> > >> >> >
> > >> >> > One option would be to try to avoid the allocation and write the lock
> > >> >> > descriptors directly into the FIFO, but this will probably get really
> > >> >> > messy as well, since you would need to carefully modify the FIFO
> > >> >> > management in several functions ...
> > >> >> >
> [...]
> 
> FWIW, I couldn't stop myself trying to finish my "write lock descriptors
> directly into FIFO" idea, once I started I was curious how it would turn
> out. I think it's actually quite elegant, the loop in bam_start_dma() is
> wrapped with LOCK and UNLOCK, later process_channel_irqs() looks at the
> FIFO again and just skips over these descriptors. lock_ce is allocated
> once at channel creation time, no other allocations are needed. (Could
> probably move lock_ce allocation to slave_config() so it's allocated
> only when locking is configured for a channel).
> 
> See diff below. It doesn't crash badly in a quick test, but didn't check
> in detail if the locking is actually working correctly. :')
> 
> I'm okay with whatever is "free of known races" and works with the use
> cases we have, so use/adapt whatever you like best.
> 
> If you want to use this, feel free to add
> Co-developed-by: Stephan Gerhold <stephan.gerhold@linaro.org>
> Signed-off-by: Stephan Gerhold <stephan.gerhold@linaro.org>
> 
> Good luck! :D
> 
> Thanks,
> Stephan
> 
> ---
>  drivers/dma/qcom/bam_dma.c       | 94 ++++++++++++++++++++++++++++++--
>  include/linux/dma/qcom_bam_dma.h | 15 +++++
>  2 files changed, 104 insertions(+), 5 deletions(-)
> 
> diff --git a/drivers/dma/qcom/bam_dma.c b/drivers/dma/qcom/bam_dma.c
> index f3e713a5259c..864531dca2a6 100644
> --- a/drivers/dma/qcom/bam_dma.c
> +++ b/drivers/dma/qcom/bam_dma.c
> [...]
> @@ -607,6 +620,17 @@ static int bam_alloc_chan(struct dma_chan *chan)
>  		return -ENOMEM;
>  	}
>  
> +	if (bdev->dev_data->pipe_lock_supported) {
> +		bchan->lock_ce = dma_alloc_wc(bdev->dev, sizeof(*bchan->lock_ce),
> +					      &bchan->lock_ce_phys, GFP_KERNEL);
> +		if (!bchan->lock_ce) {
> +			dev_err(bdev->dev, "Failed to allocate lock CE\n");
> +			dma_free_wc(bdev->dev, BAM_DESC_FIFO_SIZE, bchan->fifo_virt, bchan->fifo_phys);
> +			bchan->fifo_virt = NULL;
> +			return -ENOMEM;
> +		}
> +	}

Nitpicking on my own diff: This is kind of meh, since this will
typically allocate a full page just for the 16 bytes lock command
element for all channels on BAMs that support locking (which is most BAM
instances nowadays). Barely anything will use locking though (likely
just QCE and NAND, if available). With 32+ channels in the BAM on some
SoCs with 16K page size or more that's a lot of wasted space.

Probably best to go back to what you had in your patch and use
dma_map_single() / dma_sync_single_for_device(), see below (on top of
what I sent already).

Thanks,
Stephan

---
 drivers/dma/qcom/bam_dma.c | 53 +++++++++++++++++++++++---------------
 1 file changed, 32 insertions(+), 21 deletions(-)

diff --git a/drivers/dma/qcom/bam_dma.c b/drivers/dma/qcom/bam_dma.c
index 864531dca2a6..93ce99125879 100644
--- a/drivers/dma/qcom/bam_dma.c
+++ b/drivers/dma/qcom/bam_dma.c
@@ -433,7 +433,7 @@ struct bam_chan {
 	struct list_head node;
 
 	/* BAM locking infrastructure */
-	struct bam_cmd_element *lock_ce;
+	struct bam_cmd_element lock_ce;
 	dma_addr_t lock_ce_phys;
 	bool locking_enabled;
 	bool bam_locked;
@@ -620,17 +620,6 @@ static int bam_alloc_chan(struct dma_chan *chan)
 		return -ENOMEM;
 	}
 
-	if (bdev->dev_data->pipe_lock_supported) {
-		bchan->lock_ce = dma_alloc_wc(bdev->dev, sizeof(*bchan->lock_ce),
-					      &bchan->lock_ce_phys, GFP_KERNEL);
-		if (!bchan->lock_ce) {
-			dev_err(bdev->dev, "Failed to allocate lock CE\n");
-			dma_free_wc(bdev->dev, BAM_DESC_FIFO_SIZE, bchan->fifo_virt, bchan->fifo_phys);
-			bchan->fifo_virt = NULL;
-			return -ENOMEM;
-		}
-	}
-
 	if (bdev->active_channels++ == 0 && bdev->powered_remotely)
 		bam_reset(bdev);
 
@@ -665,13 +654,15 @@ static void bam_free_chan(struct dma_chan *chan)
 	scoped_guard(spinlock_irqsave, &bchan->vc.lock)
 		bam_reset_channel(bchan);
 
+	if (bchan->lock_ce_phys) {
+		dma_unmap_single(bdev->dev, bchan->lock_ce_phys,
+				 sizeof(bchan->lock_ce), DMA_TO_DEVICE);
+		bchan->lock_ce_phys = 0;
+	}
+
 	dma_free_wc(bdev->dev, BAM_DESC_FIFO_SIZE, bchan->fifo_virt,
 		    bchan->fifo_phys);
 	bchan->fifo_virt = NULL;
-	if (bchan->lock_ce) {
-		dma_free_wc(bdev->dev, sizeof(*bchan->lock_ce), bchan->lock_ce, bchan->lock_ce_phys);
-		bchan->lock_ce = NULL;
-	}
 
 	/* mask irq for pipe/channel */
 	val = readl_relaxed(bam_addr(bdev, 0, BAM_IRQ_SRCS_MSK_EE));
@@ -693,6 +684,24 @@ static void bam_free_chan(struct dma_chan *chan)
 	pm_runtime_put_autosuspend(bdev->dev);
 }
 
+static bool bam_map_lock_ce(struct bam_chan *bchan)
+{
+	if (bchan->lock_ce_phys) {
+		dma_sync_single_for_device(bchan->bdev->dev, bchan->lock_ce_phys,
+					   sizeof(bchan->lock_ce), DMA_TO_DEVICE);
+		return true;
+	}
+
+	bchan->lock_ce_phys = dma_map_single(bchan->bdev->dev, &bchan->lock_ce,
+					     sizeof(bchan->lock_ce), DMA_TO_DEVICE);
+	if (dma_mapping_error(bchan->bdev->dev, bchan->lock_ce_phys)) {
+		bchan->lock_ce_phys = 0;
+		return false;
+	}
+
+	return true;
+}
+
 /**
  * bam_slave_config - set slave configuration for channel
  * @chan: dma channel
@@ -717,11 +726,13 @@ static int bam_slave_config(struct dma_chan *chan,
 		if (cfg->direction != DMA_MEM_TO_DEV)
 			return -EINVAL;
 
-		if (bchan->lock_ce) {
-			bam_prep_ce_le32(bchan->lock_ce, peripheral_cfg->lock_scratchpad_addr,
-					 BAM_WRITE_COMMAND, 0);
-			bchan->locking_enabled = true;
-		}
+		bam_prep_ce_le32(&bchan->lock_ce, peripheral_cfg->lock_scratchpad_addr,
+				 BAM_WRITE_COMMAND, 0);
+
+		if (!bam_map_lock_ce(bchan))
+			return -ENOMEM;
+
+		bchan->locking_enabled = true;
 	} else {
 		/* Don't touch lock_ce here, it might still be used by issued descriptors */
 		bchan->locking_enabled = false;
-- 
2.54.0



^ permalink raw reply related

* Re: [PATCH net-next v5 14/15] ixd: add the core initialization
From: Maciej Fijalkowski @ 2026-07-23 10:28 UTC (permalink / raw)
  To: Simon Horman
  Cc: anthony.l.nguyen, davem, kuba, pabeni, edumazet, andrew+netdev,
	netdev, larysa.zaremba, przemyslaw.kitszel, aleksander.lobakin,
	sridhar.samudrala, michal.swiatkowski, emil.s.tantilov,
	madhu.chittim, joshua.a.hay, jacob.e.keller,
	jayaprakash.shanmugam, jiri, corbet, richardcochran, skhan,
	linux-doc, Bharath.r
In-Reply-To: <20260722161836.544694-1-horms@kernel.org>

On Wed, Jul 22, 2026 at 05:18:36PM +0100, Simon Horman wrote:

Simon,

Larysa already responded to Sashiko comments. Did you miss this?
Do you have a bot configured for that AI output forwarding?

> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> https://sashiko.dev/#/patchset/20260715180042.1972010-2-anthony.l.nguyen@intel.com?part=14
> ---
> --- Patch [14]: [PATCH net-next v5 14/15] ixd: add the core initialization ---
> commit 43763d63c75ee5bcdd9189c20cf8b272997bdfec
> Author: Larysa Zaremba <larysa.zaremba@intel.com>
> 
> ixd: add the core initialization
> 
> This patch initializes the core when the mailbox is set up. The driver
> confirms the virtchnl version, requests necessary capabilities, and sets up
> the message handling framework.
> 
> > diff --git a/drivers/net/ethernet/intel/ixd/ixd_ctlq.c b/drivers/net/ethernet/intel/ixd/ixd_ctlq.c
> > index 0000000000000..6d658be0695d4
> > --- /dev/null
> > +++ b/drivers/net/ethernet/intel/ixd/ixd_ctlq.c
> 
> [ ... ]
> 
> > @@ -0,0 +1,146 @@
> 
> [ ... ]
> 
> > +void ixd_ctlq_rx_task(struct work_struct *work)
> > +{
> > +	struct ixd_adapter *adapter;
> > +
> > +	adapter = container_of(work, struct ixd_adapter, mbx_task.work);
> > +
> > +	queue_delayed_work(system_unbound_wq, &adapter->mbx_task,
> > +			   msecs_to_jiffies(300));
> > +
> > +	ixd_ctlq_recv_mb_msg(adapter);
> > +}
> 
> [Severity: High]
> Does queueing the delayed work before executing the main logic create a race
> condition? If cancel_delayed_work_sync() is called (for example, during
> teardown in ixd_deinit_dflt_mbx()), try_to_grab_pending() could successfully
> delete the newly queued timer and return immediately, assuming the work is
> fully cancelled.
> 
> This would bypass flush_work() and allow the teardown path to free
> adapter->xnm and set adapter->arq to NULL while ixd_ctlq_recv_mb_msg() is
> still running, potentially causing a use-after-free or NULL pointer
> dereference. Should the re-arming be moved to the end of the task?
> 
> > diff --git a/drivers/net/ethernet/intel/ixd/ixd_lib.c b/drivers/net/ethernet/intel/ixd/ixd_lib.c
> > index afc413d3650fb..ce870aba143d4 100644
> > --- a/drivers/net/ethernet/intel/ixd/ixd_lib.c
> > +++ b/drivers/net/ethernet/intel/ixd/ixd_lib.c
> 
> [ ... ]
> 
> > @@ -136,8 +145,27 @@ void ixd_init_task(struct work_struct *work)
> 
> [ ... ]
> 
> > +	libie_ctlq_xn_shutdown(adapter->xnm);
> > +	ixd_trigger_reset(adapter);
> > +	ixd_deinit_dflt_mbx(adapter);
> 
> [Severity: High]
> Is it safe to trigger a hardware reset before cancelling the mailbox polling
> task? ixd_deinit_dflt_mbx() is responsible for cancelling the mbx_task, but
> calling ixd_trigger_reset() first means the polling task could run while the
> hardware is resetting.
> 
> During reset, MMIO reads or DMA ring memory might temporarily contain invalid
> states, and the still-running mbx_task might process corrupted descriptors.
> Should the polling task be cancelled before asserting the hardware reset here,
> as well as in ixd_remove() in ixd_main.c?

^ permalink raw reply

* Re: [PATCH 11/16] docs/zh_TW: process: localize terminology in howto.rst
From: Weijie Yuan @ 2026-07-23 10:53 UTC (permalink / raw)
  To: Chen-Yu Yeh
  Cc: Jonathan Corbet, Alex Shi, Dongliang Mu, Yanteng Si, Hu Haowen,
	linux-doc, linux-kernel
In-Reply-To: <20260721215542.98435-12-chenyou910331@gmail.com>

On Wed, Jul 22, 2026 at 05:55:37AM +0800, Chen-Yu Yeh wrote:
> -Linux內核大部分是由C語言寫成的,一些體系結構相關的代碼用到了彙編語言。要
> -參與內核開發,你必須精通C語言。除非你想爲某個架構開發底層代碼,否則你並
> -不需要了解(任何體系結構的)彙編語言。下面列舉的書籍雖然不能替代紮實的C
> -語言教育和多年的開發經驗,但如果需要的話,做爲參考還是不錯的:
> +Linux核心大部分是由C語言寫成的,一些體系結構相關的程式碼用到了組譯語言。要
> +參與核心開發,你必須精通C語言。除非你想為某個架構開發底層程式碼,否則你並
> +不需要了解(任何體系結構的)組譯語言。下面列舉的書籍雖然不能替代紮實的C
> +語言教育和多年的開發經驗,但如果需要的話,做為參考還是不錯的:
>  
>   - "The C Programming Language" by Kernighan and Ritchie [Prentice Hall]
> -   《C程序設計語言(第2版·新版)》(徐寶文 李志 譯)[機械工業出版社]
> +   《C程式設計語言(第2版·新版)》(徐寶文 李志 譯)[機械工業出版社]
>   - "Practical C Programming" by Steve Oualline [O'Reilly]
> -   《實用C語言編程(第三版)》(郭大海 譯)[中國電力出版社]
> +   《實用C語言程式設計(第三版)》(郭大海 譯)[中國電力出版社]
>   - "C:  A Reference Manual" by Harbison and Steele [Prentice Hall]
>     《C語言參考手冊(原書第5版)》(邱仲潘 等譯)[機械工業出版社]

Is there really a traditional Chinese version available?

If not, it's better to keep it as-is.

But obviously it's not the actual purpose of this patch.

> -Linux內核使用GNU C和GNU工具鏈開發。雖然它遵循ISO C11標準,但也用到了一些
> -標準中沒有定義的擴展。內核是自給自足的C環境,不依賴於標準C庫的支持,所以
> -並不支持C標準中的部分定義。比如long long類型的大數除法和浮點運算就不允許
> -使用。有時候確實很難弄清楚內核對工具鏈的要求和它所使用的擴展,不幸的是目
> -前還沒有明確的參考資料可以解釋它們。請查閱gcc信息頁(使用「info gcc」命令
> -顯示)獲得一些這方面信息。
> +Linux核心使用GNU C和GNU工具鏈開發。雖然它遵循ISO C11標準,但也用到了一些
> +標準中沒有定義的擴展。核心是自給自足的C環境,不依賴於標準C庫的支援,所以
> +並不支援C標準中的部分定義。比如long long類型的大數除法和浮點運算就不允許
> +使用。有時候確實很難弄清楚核心對工具鏈的要求和它所使用的擴展,不幸的是目
> +前還沒有明確的參考資料可以解釋它們。請查閱gcc資訊頁(使用「info gcc」命令
> +顯示)獲得一些這方面資訊。
>  
> -請記住你是在學習怎麼和已經存在的開發社區打交道。它由一羣形形色色的人組成,
> -他們對代碼、風格和過程有著很高的標準。這些標準是在長期實踐中總結出來的,
> +請記住你是在學習怎麼和已經存在的開發社群打交道。它由一羣形形色色的人組成,
> +他們對程式碼、風格和過程有著很高的標準。這些標準是在長期實踐中總結出來的,
>  適應於地理上分散的大型開發團隊。它們已經被很好得整理成檔,建議你在開發
> -之前儘可能多的學習這些標準,而不要期望別人來適應你或者你公司的行爲方式。
> +之前儘可能多的學習這些標準,而不要期望別人來適應你或者你公司的行為方式。

[...]

> -如何成爲內核開發者
> +如何成為核心開發者
>  ------------------
> -如果你對Linux內核開發一無所知,你應該訪問「Linux內核新手」計劃:
> +如果你對Linux核心開發一無所知,你應該存取「Linux核心新手」計劃:
>  
>  	https://kernelnewbies.org

Should we use "造訪" instead of "存取" here?

Aside: the original translation was a little stiff. Better change to
like: "如果你对Linux核心開發一無所知,請參閱/參照/造訪這個網站"

>  
> -它擁有一個可以問各種最基本的內核開發問題的郵件列表(在提問之前一定要記得
> +它擁有一個可以問各種最基本的核心開發問題的郵件列表(在提問之前一定要記得
>  查找已往的郵件,確認是否有人已經回答過相同的問題)。它還擁有一個可以獲得
> -實時反饋的IRC聊天頻道,以及大量對於學習Linux內核開發相當有幫助的文檔。
> +實時反饋的IRC聊天頻道,以及大量對於學習Linux核心開發相當有幫助的文件。
>  
> -網站簡要介紹了原始碼組織結構、子系統劃分以及目前正在進行的項目(包括內核
> -中的和單獨維護的)。它還提供了一些基本的幫助信息,比如如何編譯內核和打補
> +網站簡要介紹了原始碼組織結構、子系統劃分以及目前正在進行的專案(包括核心
> +中的和單獨維護的)。它還提供了一些基本的幫助資訊,比如如何編譯核心和打補
>  丁。
>  
> -如果你想加入內核開發社區並協助完成一些任務,卻找不到從哪裡開始,可以訪問
> -「Linux內核房管員」計劃:
> +如果你想加入核心開發社群並協助完成一些任務,卻找不到從哪裡開始,可以存取
> +「Linux核心房管員」計劃:

Ditto.

>  
>  	https://kernelnewbies.org/KernelJanitors
>  
> -這是極佳的起點。它提供一個相對簡單的任務列表,列出內核代碼中需要被重新
> -整理或者改正的地方。通過和負責這個計劃的開發者們一同工作,你會學到將補丁
> -集成進內核的基本原理。如果還沒有決定下一步要做什麼的話,你還可能會得到方
> +這是極佳的起點。它提供一個相對簡單的任務列表,列出核心程式碼中需要被重新
> +整理或者改正的地方。透過和負責這個計劃的開發者們一同工作,你會學到將補丁
> +整合進核心的基本原理。如果還沒有決定下一步要做什麼的話,你還可能會得到方
>  向性的指點。
>  
> -在真正動手修改內核代碼之前,理解要修改的代碼如何運作是必需的。要達到這個
> -目的,沒什麼辦法比直接讀代碼更有效了(大多數花招都會有相應的注釋),而且
> -一些特製的工具還可以提供幫助。例如,「Linux代碼交叉引用」項目就是一個值得
> +在真正動手修改核心程式碼之前,理解要修改的程式碼如何運作是必需的。要達到這個
> +目的,沒什麼辦法比直接讀程式碼更有效了(大多數花招都會有相應的註解),而且
> +一些特製的工具還可以提供幫助。例如,「Linux程式碼交叉引用」專案就是一個值得
>  特別推薦的幫助工具,它將原始碼顯示在有編目和索引的網頁上。其中一個更新及
> -時的內核源碼庫,可以通過以下地址訪問:
> +時的核心源碼庫,可以透過以下地址存取:

Ditto.

> -Linux-next 集成測試樹
> +Linux-next 整合測試樹
>  ---------------------
>  
> -在將子系統樹的更新合併到主線樹之前,需要對它們進行集成測試。爲此,存在一個
> -特殊的測試存儲庫,其中幾乎每天都會提取所有子系統樹:
> +在將子系統樹的更新合併到主線樹之前,需要對它們進行整合測試。為此,存在一個
> +特殊的測試儲存庫,其中幾乎每天都會提取所有子系統樹:
>  
> -        https://git.kernel.org/?p=linux/kernel/git/next/linux-next.git
> +        https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git
>  
> -通過這種方式,Linux-next 對下一個合併階段將進入主線內核的內容給出了一個概要
> -展望。非常歡冒險的測試者運行測試Linux-next。
> +透過這種方式,Linux-next 對下一個合併階段將進入主線核心的內容給出了一個概要
> +展望。非常歡冒險的測試者執行測試Linux-next。

非常歡迎?

>  報告bug
>  -------
>  
> -bugzilla.kernel.org是Linux內核開發者們用來跟蹤內核Bug的網站。我們鼓勵用
> -戶在這個工具中報告找到的所有bug。如何使用內核bugzilla的細節請訪問:
> +bugzilla.kernel.org是Linux核心開發者們用來追蹤核心Bug的網站。我們鼓勵用
> +戶在這個工具中報告找到的所有bug。如何使用核心bugzilla的細節請存取:

Ditto.

>  
>  	http://test.kernel.org/bugzilla/faq.html
>  
> -內核源碼主目錄中的:ref:`admin-guide/reporting-bugs.rst <reportingbugs>`
> -文件里有一個很好的模板。它指導用戶如何報告可能的內核bug以及需要提供哪些信息
> -來幫助內核開發者們找到問題的根源。
> +核心源碼主目錄中的:ref:`admin-guide/reporting-bugs.rst <reportingbugs>`
> +文件裡有一個很好的模板。它指導使用者如何報告可能的核心bug以及需要提供哪些資訊
> +來幫助核心開發者們找到問題的根源。
>  
>  
>  利用bug報告
>  -----------
>  
> -練習內核開發技能的最好辦法就是修改其他人報告的bug。你不光可以幫助內核變
> +練習核心開發技能的最好辦法就是修改其他人報告的bug。你不光可以幫助核心變
>  得更加穩定,還可以學會如何解決實際問題從而提高自己的技能,並且讓其他開發
> -者感受到你的存在。修改bug是贏得其他開發者讚譽的最好辦法,因爲並不是很多
> +者感受到你的存在。修改bug是贏得其他開發者讚譽的最好辦法,因為並不是很多
>  人都喜歡浪費時間去修改別人報告的bug。
>  
> -要嘗試修改已知的bug,請訪問 http://bugzilla.kernel.org 網址。
> +要嘗試修改已知的bug,請存取 http://bugzilla.kernel.org 網址。

Ditto.

>  
>  
>  郵件列表
>  --------
>  
> -正如上面的文檔所描述,大多數的骨幹內核開發者都加入了Linux Kernel郵件列
> +正如上面的文件所描述,大多數的骨幹核心開發者都加入了Linux Kernel郵件列
>  表。如何訂閱和退訂列表的細節可以在這裡找到:
>  
> -	http://vger.kernel.org/vger-lists.html#linux-kernel
> +	https://subspace.kernel.org/subscribing.html
>  
>  網上很多地方都有這個郵件列表的存檔(archive)。可以使用搜尋引擎來找到這些
>  存檔。比如:
>  
> -	https://lore.kernel.org/lkml/
> +	https://lore.kernel.org/linux-kernel/

Note:

I don't know what happened. This change is from commit 413e775efaec9b452
("Documentation: fix links to mailing list services") but now if you
visit /linux-kernel/, you will still be redirected to /lkml/ ?

So what's the point of changing, or perhaps KR forgot to change it back
when he chose to continue using the classic "lkml".

^ permalink raw reply

* [PATCH RFC 0/7] fs: add failfs
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc

nullfs provides a permanently empty and immutable directory. Lookups
fail with ENOENT. The directory can be opened, read, stat, mounted upon.
It behaves like nothing is there.

Add its counterpart failfs where the semantics are not "there is
nothing here" but "nothing is supported here". Every operation that
reaches the filesystem fails with EOPNOTSUPP. Even statfs()/fstatfs()
fail so the filesystem cannot be discovered through an fd to it.

EOPNOTSUPP rather than a permission errno keeps that coherent. There
is no permission model in which anything could ever be allowed and
EACCES or EPERM would merely suggest that different credentials might
succeed while EIO would suggest corruption. It also makes hitting the
failfs boundary mostly quite dinstinguishable. A task anchoring its
lookups at real directory file descriptors may be able to tell a failfs
refusal from an ordinary permission failure. I wouldn't go so far as
guaranteeing that but it should mostly work.

The root cannot be opened at all not even with O_PATH. It is never
reached by a lookup in a parent directory. The only way to a path-walk
terminal at the root is a jump through a /proc/<pid>/{root,cwd} magic
link or by mountpoint traversal. The root also refuses
->d_weak_revalidate() which the VFS calls for jumped terminals. That
closes every remaining way to reference it. An O_PATH
open is refused and name_to_handle_at() cannot encode it into a file
handle, and following a magic link into it fails. A plain readlink() of
such a link still works and shows "/".

There is a single instance of failfs mounted during early boot via
kern_mount() making it logically distinct from every mount namespace.

Since the mount is a member of no mount namespace mounting onto it
fails. So nothing can ever be mounted on top of it. It cannot be cloned
via OPEN_TREE_CLONE and it does not show up in statmount()/listmount()
or /proc/<pid>/mountinfo. The filesystem is not registered so it is
not visible in /proc/filesystems and cannot be mounted from userspace.

This lets tasks shed their filesystem state completely. A process with
its root directory or working directory in failfs must anchor every path
lookup at an explicit file descriptor or is doomed to fail any lookup.
Absolute paths, absolute symlinks, and AT_FDCWD-relative lookups
simply fail. Followup patches will expose it via a new FD_FAILFS_ROOT
file descriptor sentinel understood by fchdir() and the new fchroot()
system call.

Fun fact, because of how dynamic binary execution work with PT_INTERP
this also currently prevents execution of dynamic binaries because
loaders have absolute paths (see selftests).

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Christian Brauner (7):
      fs: add failfs
      fs: add FD_FAILFS_ROOT and support it in fchdir()
      fs: add fchroot()
      fs: support FD_FAILFS_ROOT in fchroot()
      arch: hookup fchroot() system call
      selftests/filesystems: add failfs selftests
      Documentation: add failfs documentation

 Documentation/filesystems/failfs.rst               |  64 +++
 Documentation/filesystems/index.rst                |   1 +
 arch/alpha/kernel/syscalls/syscall.tbl             |   1 +
 arch/arm/tools/syscall.tbl                         |   1 +
 arch/arm64/tools/syscall_32.tbl                    |   1 +
 arch/m68k/kernel/syscalls/syscall.tbl              |   1 +
 arch/microblaze/kernel/syscalls/syscall.tbl        |   1 +
 arch/mips/kernel/syscalls/syscall_n32.tbl          |   1 +
 arch/mips/kernel/syscalls/syscall_n64.tbl          |   1 +
 arch/mips/kernel/syscalls/syscall_o32.tbl          |   1 +
 arch/parisc/kernel/syscalls/syscall.tbl            |   1 +
 arch/powerpc/kernel/syscalls/syscall.tbl           |   1 +
 arch/s390/kernel/syscalls/syscall.tbl              |   1 +
 arch/sh/kernel/syscalls/syscall.tbl                |   1 +
 arch/sparc/kernel/syscalls/syscall.tbl             |   1 +
 arch/x86/entry/syscalls/syscall_32.tbl             |   1 +
 arch/x86/entry/syscalls/syscall_64.tbl             |   1 +
 arch/xtensa/kernel/syscalls/syscall.tbl            |   1 +
 fs/Makefile                                        |   2 +-
 fs/failfs.c                                        | 155 +++++++
 fs/internal.h                                      |   3 +
 fs/namespace.c                                     |   1 +
 fs/open.c                                          |  58 ++-
 include/linux/syscalls.h                           |   1 +
 include/uapi/asm-generic/unistd.h                  |   6 +-
 include/uapi/linux/fcntl.h                         |   1 +
 include/uapi/linux/magic.h                         |   1 +
 scripts/syscall.tbl                                |   1 +
 tools/testing/selftests/Makefile                   |   1 +
 .../selftests/filesystems/failfs/.gitignore        |   2 +
 .../testing/selftests/filesystems/failfs/Makefile  |   5 +
 .../selftests/filesystems/failfs/failfs_test.c     | 506 +++++++++++++++++++++
 32 files changed, 821 insertions(+), 3 deletions(-)
---
base-commit: 1590cf0329716306e948a8fc29f1d3ee87d3989f
change-id: 20260723-work-failfs-d86a0c1db48d


^ permalink raw reply

* [PATCH RFC 1/7] fs: add failfs
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>

nullfs provides a permanently empty and immutable directory. Lookups
fail with ENOENT. The directory can be opened, read, stat, mounted upon.
It behaves like nothing is there.

Add its counterpart failfs where the semantics are not "there is
nothing here" but "nothing is supported here". Every operation that
reaches the filesystem fails with EOPNOTSUPP. Even statfs()/fstatfs()
fail so the filesystem cannot be discovered through an fd to it.

EOPNOTSUPP rather than a permission errno keeps that coherent. There
is no permission model in which anything could ever be allowed and
EACCES or EPERM would merely suggest that different credentials might
succeed while EIO would suggest corruption. It also makes hitting the
failfs boundary mostly quite dinstinguishable. A task anchoring its
lookups at real directory file descriptors may be able to tell a failfs
refusal from an ordinary permission failure. I wouldn't go so far as
guaranteeing that but it should mostly work.

The root cannot be opened at all not even with O_PATH. It is never
reached by a lookup in a parent directory. The only way to a path-walk
terminal at the root is a jump through a /proc/<pid>/{root,cwd} magic
link or by mountpoint traversal. The root also refuses
->d_weak_revalidate() which the VFS calls for jumped terminals. That
closes every remaining way to reference it. An O_PATH
open is refused and name_to_handle_at() cannot encode it into a file
handle, and following a magic link into it fails. A plain readlink() of
such a link still works and shows "/".

There is a single instance of failfs mounted during early boot via
kern_mount() making it logically distinct from every mount namespace.

Since the mount is a member of no mount namespace mounting onto it
fails. So nothing can ever be mounted on top of it. It cannot be cloned
via OPEN_TREE_CLONE and it does not show up in statmount()/listmount()
or /proc/<pid>/mountinfo. The filesystem is not registered so it is
not visible in /proc/filesystems and cannot be mounted from userspace.

This lets tasks shed their filesystem state completely. A process with
its root directory or working directory in failfs must anchor every path
lookup at an explicit file descriptor or is doomed to fail any lookup.
Absolute paths, absolute symlinks, and AT_FDCWD-relative lookups
simply fail. Followup patches will expose it via a new FD_FAILFS_ROOT
file descriptor sentinel understood by fchdir() and the new fchroot()
system call.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/Makefile                |   2 +-
 fs/failfs.c                | 144 +++++++++++++++++++++++++++++++++++++++++++++
 fs/internal.h              |   2 +
 fs/namespace.c             |   1 +
 include/uapi/linux/magic.h |   1 +
 5 files changed, 149 insertions(+), 1 deletion(-)

diff --git a/fs/Makefile b/fs/Makefile
index 89a8a9d207d1..73b6cab7738e 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -16,7 +16,7 @@ obj-y :=	open.o read_write.o file_table.o super.o \
 		stack.o fs_struct.o statfs.o fs_pin.o nsfs.o \
 		fs_dirent.o fs_context.o fs_parser.o fsopen.o init.o \
 		kernel_read_file.o mnt_idmapping.o remap_range.o pidfs.o \
-		file_attr.o fserror.o nullfs.o
+		file_attr.o fserror.o nullfs.o failfs.o
 
 obj-$(CONFIG_BUFFER_HEAD)	+= buffer.o mpage.o
 obj-$(CONFIG_PROC_FS)		+= proc_namespace.o
diff --git a/fs/failfs.c b/fs/failfs.c
new file mode 100644
index 000000000000..f9d4ba791928
--- /dev/null
+++ b/fs/failfs.c
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Christian Brauner <brauner@kernel.org> */
+#include <linux/fs.h>
+#include <linux/fs/super_types.h>
+#include <linux/fs_context.h>
+#include <linux/magic.h>
+#include <linux/mount.h>
+
+#include "internal.h"
+
+static struct path failfs_root_path = {};
+
+void failfs_get_root(struct path *path)
+{
+	*path = failfs_root_path;
+	path_get(path);
+}
+
+static int failfs_permission(struct mnt_idmap *idmap, struct inode *inode,
+			     int mask)
+{
+	return -EOPNOTSUPP;
+}
+
+static struct dentry *failfs_lookup(struct inode *dir, struct dentry *dentry,
+				    unsigned int flags)
+{
+	/* Unreachable: ->permission() already failed the walk. */
+	return ERR_PTR(-EOPNOTSUPP);
+}
+
+static int failfs_getattr(struct mnt_idmap *idmap, const struct path *path,
+			  struct kstat *stat, u32 request_mask,
+			  unsigned int query_flags)
+{
+	return -EOPNOTSUPP;
+}
+
+static const struct inode_operations failfs_dir_inode_operations = {
+	.permission	= failfs_permission,
+	.lookup		= failfs_lookup,
+	.getattr	= failfs_getattr,
+};
+
+static const struct file_operations failfs_dir_operations = {};
+
+static int failfs_d_weak_revalidate(struct dentry *dentry, unsigned int flags)
+{
+	/*
+	 * The root is only ever reached as a path-walk terminal by jumping
+	 * to it: as "/" when it is the caller's root, or through a
+	 * /proc/<pid>/{root,cwd} magic link. ->permission() already fails
+	 * every walk of a component, but a jump lands on the root without
+	 * one. Refuse here too so the root cannot be pinned by an O_PATH
+	 * open or encoded into a file handle.
+	 */
+	return -EOPNOTSUPP;
+}
+
+static const struct dentry_operations failfs_dentry_operations = {
+	.d_weak_revalidate	= failfs_d_weak_revalidate,
+};
+
+static int failfs_statfs(struct dentry *dentry, struct kstatfs *buf)
+{
+	return -EOPNOTSUPP;
+}
+
+static const struct super_operations failfs_super_operations = {
+	.statfs	= failfs_statfs,
+};
+
+static int failfs_fill_super(struct super_block *s, struct fs_context *fc)
+{
+	struct inode *inode;
+
+	s->s_maxbytes		= MAX_LFS_FILESIZE;
+	s->s_blocksize		= PAGE_SIZE;
+	s->s_blocksize_bits	= PAGE_SHIFT;
+	s->s_magic		= FAIL_FS_MAGIC;
+	s->s_op			= &failfs_super_operations;
+	s->s_export_op		= NULL;
+	s->s_xattr		= NULL;
+	s->s_time_gran		= 1;
+	s->s_d_flags		= 0;
+
+	inode = new_inode(s);
+	if (!inode)
+		return -ENOMEM;
+
+	/* failfs supports no operations... */
+	inode->i_mode	= S_IFDIR;
+	set_nlink(inode, 2);
+	inode->i_op	= &failfs_dir_inode_operations;
+	inode->i_fop	= &failfs_dir_operations;
+	simple_inode_init_ts(inode);
+	inode->i_ino	= 1;
+	/* ... and is immutable. */
+	inode->i_flags |= S_IMMUTABLE;
+
+	set_default_d_op(s, &failfs_dentry_operations);
+	s->s_root = d_make_root(inode);
+	if (!s->s_root)
+		return -ENOMEM;
+
+	return 0;
+}
+
+static int failfs_get_tree(struct fs_context *fc)
+{
+	return get_tree_single(fc, failfs_fill_super);
+}
+
+static const struct fs_context_operations failfs_context_ops = {
+	.get_tree	= failfs_get_tree,
+};
+
+static int failfs_init_fs_context(struct fs_context *fc)
+{
+	fc->ops		= &failfs_context_ops;
+	fc->global	= true;
+	fc->sb_flags	|= SB_NOUSER;
+	fc->s_iflags	|= SB_I_NOEXEC | SB_I_NODEV;
+	return 0;
+}
+
+static struct file_system_type failfs_fs_type = {
+	.name			= "failfs",
+	.init_fs_context	= failfs_init_fs_context,
+	.kill_sb		= kill_anon_super,
+};
+
+void __init failfs_init(void)
+{
+	struct vfsmount *mnt;
+
+	/* A single instance that is member of no mount namespace. */
+	mnt = kern_mount(&failfs_fs_type);
+	if (IS_ERR(mnt))
+		panic("VFS: Failed to create failfs");
+
+	failfs_root_path.mnt	= mnt;
+	failfs_root_path.dentry	= mnt->mnt_root;
+}
diff --git a/fs/internal.h b/fs/internal.h
index 355d93f92208..b6d38a5794eb 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -362,3 +362,5 @@ int anon_inode_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		       struct iattr *attr);
 void pidfs_get_root(struct path *path);
 void nsfs_get_root(struct path *path);
+void failfs_get_root(struct path *path);
+void __init failfs_init(void);
diff --git a/fs/namespace.c b/fs/namespace.c
index 3d5cd5bf3b05..87c365f2f82b 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -6274,6 +6274,7 @@ void __init mnt_init(void)
 	shmem_init();
 	init_rootfs();
 	init_mount_tree();
+	failfs_init();
 }
 
 void put_mnt_ns(struct mnt_namespace *ns)
diff --git a/include/uapi/linux/magic.h b/include/uapi/linux/magic.h
index 4f2da935a76c..fd5f0e95648e 100644
--- a/include/uapi/linux/magic.h
+++ b/include/uapi/linux/magic.h
@@ -105,5 +105,6 @@
 #define PID_FS_MAGIC		0x50494446	/* "PIDF" */
 #define GUEST_MEMFD_MAGIC	0x474d454d	/* "GMEM" */
 #define NULL_FS_MAGIC		0x4E554C4C	/* "NULL" */
+#define FAIL_FS_MAGIC		0x4641494C	/* "FAIL" */
 
 #endif /* __LINUX_MAGIC_H__ */

-- 
2.53.0


^ permalink raw reply related

* [PATCH RFC 2/7] fs: add FD_FAILFS_ROOT and support it in fchdir()
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>

Add a new file descriptor sentinel FD_FAILFS_ROOT following
FD_PIDFS_ROOT and FD_NSFS_ROOT and teach fchdir() to accept it. A
process calling fchdir(FD_FAILFS_ROOT) moves its working directory
into failfs. Every AT_FDCWD-relative lookup afterwards fails with
EOPNOTSUPP including "." and ".." and getcwd() reports the working
directory as unreachable from the process root by returning a path
prefixed with "(unreachable)". Lookups relative to explicit directory
file descriptors are unaffected.

The sentinel is the only way in. No privilege or gating is required.
Setting the working directory to a directory in which every operation
fails grants nothing and loses nothing that closing file descriptors
couldn't lose. An unlinked working directory behaves the same way today
modulo errno. The working directory also plays no role in confining ".."
resolution so no boundary is weakened.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/failfs.c                | 11 +++++++++++
 fs/internal.h              |  1 +
 fs/open.c                  |  5 ++++-
 include/uapi/linux/fcntl.h |  1 +
 4 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/fs/failfs.c b/fs/failfs.c
index f9d4ba791928..f37e6af9c03c 100644
--- a/fs/failfs.c
+++ b/fs/failfs.c
@@ -3,6 +3,7 @@
 #include <linux/fs.h>
 #include <linux/fs/super_types.h>
 #include <linux/fs_context.h>
+#include <linux/fs_struct.h>
 #include <linux/magic.h>
 #include <linux/mount.h>
 
@@ -124,6 +125,16 @@ static int failfs_init_fs_context(struct fs_context *fc)
 	return 0;
 }
 
+int failfs_current_chdir(void)
+{
+	struct path path;
+
+	failfs_get_root(&path);
+	set_fs_pwd(current->fs, &path);
+	path_put(&path);
+	return 0;
+}
+
 static struct file_system_type failfs_fs_type = {
 	.name			= "failfs",
 	.init_fs_context	= failfs_init_fs_context,
diff --git a/fs/internal.h b/fs/internal.h
index b6d38a5794eb..b3c88d999dec 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -364,3 +364,4 @@ void pidfs_get_root(struct path *path);
 void nsfs_get_root(struct path *path);
 void failfs_get_root(struct path *path);
 void __init failfs_init(void);
+int failfs_current_chdir(void);
diff --git a/fs/open.c b/fs/open.c
index 408925d7bd0b..56b6032d4d81 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -570,9 +570,12 @@ SYSCALL_DEFINE1(chdir, const char __user *, filename)
 
 SYSCALL_DEFINE1(fchdir, unsigned int, fd)
 {
-	CLASS(fd_raw, f)(fd);
 	int error;
 
+	if ((int)fd == FD_FAILFS_ROOT)
+		return failfs_current_chdir();
+
+	CLASS(fd_raw, f)(fd);
 	if (fd_empty(f))
 		return -EBADF;
 
diff --git a/include/uapi/linux/fcntl.h b/include/uapi/linux/fcntl.h
index aadfbf6e0cb3..e43e3de3e9ee 100644
--- a/include/uapi/linux/fcntl.h
+++ b/include/uapi/linux/fcntl.h
@@ -124,6 +124,7 @@ struct delegation {
 
 #define FD_PIDFS_ROOT			-10002 /* Root of the pidfs filesystem */
 #define FD_NSFS_ROOT			-10003 /* Root of the nsfs filesystem */
+#define FD_FAILFS_ROOT			-10004 /* Root of the failfs filesystem */
 #define FD_INVALID			-10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */
 
 /* Generic flags for the *at(2) family of syscalls. */

-- 
2.53.0


^ permalink raw reply related

* [PATCH RFC 3/7] fs: add fchroot()
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>

Add a file descriptor based counterpart to chroot(2). This has been
overdue for a long time. It is the natural companion to fchdir() and
avoids re-resolving a path that the caller already holds a file
descriptor to. No TOCTOU between resolving the target and changing the
root. It composes with modern fd-based APIs meaning it works with O_PATH
file descriptors and file descriptors to detached mount trees created
via open_tree(OPEN_TREE_CLONE).

The permission model is identical to chroot(2). Rhe caller must have
CAP_SYS_CHROOT in its user namespace, must pass MAY_EXEC | MAY_CHDIR
permission checks on the target directory, and LSMs are consulted via
the same security_path_chroot() hook.

The system call takes a flags argument for future extensibility which
must currently be zero.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/open.c                | 29 +++++++++++++++++++++++++++++
 include/linux/syscalls.h |  1 +
 2 files changed, 30 insertions(+)

diff --git a/fs/open.c b/fs/open.c
index 56b6032d4d81..c57f641f2e29 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -618,6 +618,35 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename)
 	return error;
 }
 
+SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
+{
+	int error;
+
+	if (flags)
+		return -EINVAL;
+
+	CLASS(fd_raw, f)(fd);
+	if (fd_empty(f))
+		return -EBADF;
+
+	if (!d_can_lookup(fd_file(f)->f_path.dentry))
+		return -ENOTDIR;
+
+	error = file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR);
+	if (error)
+		return error;
+
+	if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
+		return -EPERM;
+
+	error = security_path_chroot(&fd_file(f)->f_path);
+	if (error)
+		return error;
+
+	set_fs_root(current->fs, &fd_file(f)->f_path);
+	return 0;
+}
+
 int chmod_common(const struct path *path, umode_t mode)
 {
 	struct inode *inode = path->dentry->d_inode;
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 874d9067a43b..8413b624ad47 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -457,6 +457,7 @@ asmlinkage long sys_faccessat2(int dfd, const char __user *filename, int mode,
 asmlinkage long sys_chdir(const char __user *filename);
 asmlinkage long sys_fchdir(unsigned int fd);
 asmlinkage long sys_chroot(const char __user *filename);
+asmlinkage long sys_fchroot(int fd, unsigned int flags);
 asmlinkage long sys_fchmod(unsigned int fd, umode_t mode);
 asmlinkage long sys_fchmodat(int dfd, const char __user *filename,
 			     umode_t mode);

-- 
2.53.0


^ permalink raw reply related

* [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>

Allow a process to move its root directory into failfs via
fchroot(FD_FAILFS_ROOT). From that point on every absolute path lookup
and every absolute symlink fails with EOPNOTSUPP. Combined with
fchdir(FD_FAILFS_ROOT) this leaves the process with lookups anchored
at explicit directory file descriptors only. It is the fs_struct
equivalent of RESOLVE_BENEATH. This allows taks to drop their filesystem
state completely.

Callers with CAP_SYS_CHROOT in their user namespace may always do
this, mirroring chroot(2). Unprivileged callers are subject to two
requirements (which may be loosened later):

(1) no_new_privs must be set

    After entering failfs suid binaries on regular mounts remain
    reachable via inherited directory file descriptors or the working
    directory. A setuid program executing with an unusable root
    directory might be tricked by this. I'm not 100% convinced that this
    is needed but it feels more secure initially and it also forces more
    no_new_privs on userspace. So win-win imo.

(2) The caller must not already be chrooted.

    The root directory is what confines .. resolution. The failfs root
    can never be reached by walking up a real mount tree. A task whose
    root is failfs has no .. barrier left below the top of its mount
    tree. A .. walk from any real directory fd it still holds climbs
    to the mount-namespace root. Which is kinda the point if you want to
    do fd-based lookup only. If failfs prevented you from doing that
    then it doesn't make a lot of sense.

    A task that a privileged manager chrooted into a subtree could use
    chroot()ing into failfs as a way to allow for an inherited fd to
    resolve it again.

    So reject already-chrooted callers closing that issue without losing
    anything for the intended self-sandboxing use case.

There's also some thought needed around shared fs_struct state. It's
obviously possible to chroot into failfs with a shared fs_struct if the
caller has CAP_SYS_CHROOT and shares the fs_struct or if the caller is
no_new_privs and shares the fs_struct. The non-chrooted-currently
requirement still applies.

Once entered, failfs is a throw-away-the-key moment. The task is
considered chrooted so it cannot create user namespaces to regain
CAP_SYS_CHROOT. chroot()/fchroot() back out require CAP_SYS_CHROOT.

The only other exit is setns() to a mount namespace file descriptor
which requires CAP_SYS_ADMIN over the target namespace plus
CAP_SYS_CHROOT and CAP_SYS_ADMIN in the caller's user namespace and
resets both root and working directory. A process that closes or never
had such file descriptors and restricts *chdir()/*chroot()/setns() via
seccomp has thrown away the key.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/open.c | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/fs/open.c b/fs/open.c
index c57f641f2e29..8adc9f00889a 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -618,6 +618,27 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename)
 	return error;
 }
 
+static int fchroot_failfs(void)
+{
+	struct path path;
+	int error;
+
+	if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) {
+		if (!task_no_new_privs(current))
+			return -EPERM;
+		/* Moving the root to failfs lifts the old root's ".." barrier. */
+		if (current_chrooted())
+			return -EPERM;
+	}
+
+	failfs_get_root(&path);
+	error = security_path_chroot(&path);
+	if (!error)
+		set_fs_root(current->fs, &path);
+	path_put(&path);
+	return error;
+}
+
 SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
 {
 	int error;
@@ -625,6 +646,9 @@ SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
 	if (flags)
 		return -EINVAL;
 
+	if (fd == FD_FAILFS_ROOT)
+		return fchroot_failfs();
+
 	CLASS(fd_raw, f)(fd);
 	if (fd_empty(f))
 		return -EBADF;

-- 
2.53.0


^ permalink raw reply related

* [PATCH RFC 5/7] arch: hookup fchroot() system call
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>

Wire up the fchroot() system call as number 472 on (nearly) all
architectures.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 arch/alpha/kernel/syscalls/syscall.tbl      | 1 +
 arch/arm/tools/syscall.tbl                  | 1 +
 arch/arm64/tools/syscall_32.tbl             | 1 +
 arch/m68k/kernel/syscalls/syscall.tbl       | 1 +
 arch/microblaze/kernel/syscalls/syscall.tbl | 1 +
 arch/mips/kernel/syscalls/syscall_n32.tbl   | 1 +
 arch/mips/kernel/syscalls/syscall_n64.tbl   | 1 +
 arch/mips/kernel/syscalls/syscall_o32.tbl   | 1 +
 arch/parisc/kernel/syscalls/syscall.tbl     | 1 +
 arch/powerpc/kernel/syscalls/syscall.tbl    | 1 +
 arch/s390/kernel/syscalls/syscall.tbl       | 1 +
 arch/sh/kernel/syscalls/syscall.tbl         | 1 +
 arch/sparc/kernel/syscalls/syscall.tbl      | 1 +
 arch/x86/entry/syscalls/syscall_32.tbl      | 1 +
 arch/x86/entry/syscalls/syscall_64.tbl      | 1 +
 arch/xtensa/kernel/syscalls/syscall.tbl     | 1 +
 include/uapi/asm-generic/unistd.h           | 6 +++++-
 scripts/syscall.tbl                         | 1 +
 18 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/syscall.tbl
index f31b7afffc34..52e3538cc7df 100644
--- a/arch/alpha/kernel/syscalls/syscall.tbl
+++ b/arch/alpha/kernel/syscalls/syscall.tbl
@@ -511,3 +511,4 @@
 579	common	file_setattr			sys_file_setattr
 580	common	listns				sys_listns
 581	common	rseq_slice_yield		sys_rseq_slice_yield
+582	common	fchroot				sys_fchroot
diff --git a/arch/arm/tools/syscall.tbl b/arch/arm/tools/syscall.tbl
index 94351e22bfcf..55717ed32c27 100644
--- a/arch/arm/tools/syscall.tbl
+++ b/arch/arm/tools/syscall.tbl
@@ -486,3 +486,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/arm64/tools/syscall_32.tbl b/arch/arm64/tools/syscall_32.tbl
index 62d93d88e0fe..df2d1d82fb3c 100644
--- a/arch/arm64/tools/syscall_32.tbl
+++ b/arch/arm64/tools/syscall_32.tbl
@@ -483,3 +483,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/m68k/kernel/syscalls/syscall.tbl b/arch/m68k/kernel/syscalls/syscall.tbl
index 248934257101..ba7a4d8903d0 100644
--- a/arch/m68k/kernel/syscalls/syscall.tbl
+++ b/arch/m68k/kernel/syscalls/syscall.tbl
@@ -471,3 +471,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/microblaze/kernel/syscalls/syscall.tbl b/arch/microblaze/kernel/syscalls/syscall.tbl
index 223d26303627..c55a5c96f49b 100644
--- a/arch/microblaze/kernel/syscalls/syscall.tbl
+++ b/arch/microblaze/kernel/syscalls/syscall.tbl
@@ -477,3 +477,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/mips/kernel/syscalls/syscall_n32.tbl b/arch/mips/kernel/syscalls/syscall_n32.tbl
index 7430714e2b8f..9ae88e4eac61 100644
--- a/arch/mips/kernel/syscalls/syscall_n32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n32.tbl
@@ -410,3 +410,4 @@
 469	n32	file_setattr			sys_file_setattr
 470	n32	listns				sys_listns
 471	n32	rseq_slice_yield		sys_rseq_slice_yield
+472	n32	fchroot				sys_fchroot
diff --git a/arch/mips/kernel/syscalls/syscall_n64.tbl b/arch/mips/kernel/syscalls/syscall_n64.tbl
index 630aab9e5425..83dc93a0712f 100644
--- a/arch/mips/kernel/syscalls/syscall_n64.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n64.tbl
@@ -386,3 +386,4 @@
 469	n64	file_setattr			sys_file_setattr
 470	n64	listns				sys_listns
 471	n64	rseq_slice_yield		sys_rseq_slice_yield
+472	n64	fchroot				sys_fchroot
diff --git a/arch/mips/kernel/syscalls/syscall_o32.tbl b/arch/mips/kernel/syscalls/syscall_o32.tbl
index 128653112284..9c62429c9b7b 100644
--- a/arch/mips/kernel/syscalls/syscall_o32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_o32.tbl
@@ -459,3 +459,4 @@
 469	o32	file_setattr			sys_file_setattr
 470	o32	listns				sys_listns
 471	o32	rseq_slice_yield		sys_rseq_slice_yield
+472	o32	fchroot				sys_fchroot
diff --git a/arch/parisc/kernel/syscalls/syscall.tbl b/arch/parisc/kernel/syscalls/syscall.tbl
index c6331dad9461..88adc4016cce 100644
--- a/arch/parisc/kernel/syscalls/syscall.tbl
+++ b/arch/parisc/kernel/syscalls/syscall.tbl
@@ -470,3 +470,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/powerpc/kernel/syscalls/syscall.tbl b/arch/powerpc/kernel/syscalls/syscall.tbl
index 4fcc7c58a105..cfbb70039ff0 100644
--- a/arch/powerpc/kernel/syscalls/syscall.tbl
+++ b/arch/powerpc/kernel/syscalls/syscall.tbl
@@ -562,3 +562,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	nospu	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/s390/kernel/syscalls/syscall.tbl b/arch/s390/kernel/syscalls/syscall.tbl
index 09a7ef04d979..1b45e68a217b 100644
--- a/arch/s390/kernel/syscalls/syscall.tbl
+++ b/arch/s390/kernel/syscalls/syscall.tbl
@@ -398,3 +398,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/sh/kernel/syscalls/syscall.tbl b/arch/sh/kernel/syscalls/syscall.tbl
index 70b315cbe710..ace068dff0de 100644
--- a/arch/sh/kernel/syscalls/syscall.tbl
+++ b/arch/sh/kernel/syscalls/syscall.tbl
@@ -475,3 +475,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/sparc/kernel/syscalls/syscall.tbl b/arch/sparc/kernel/syscalls/syscall.tbl
index 7e71bf7fcd14..5b9fe0e8140f 100644
--- a/arch/sparc/kernel/syscalls/syscall.tbl
+++ b/arch/sparc/kernel/syscalls/syscall.tbl
@@ -517,3 +517,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b..2c172ef48dfd 100644
--- a/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,4 @@
 469	i386	file_setattr		sys_file_setattr
 470	i386	listns			sys_listns
 471	i386	rseq_slice_yield	sys_rseq_slice_yield
+472	i386	fchroot			sys_fchroot
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da..d5b6045b0090 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,7 @@
 469	common	file_setattr		sys_file_setattr
 470	common	listns			sys_listns
 471	common	rseq_slice_yield	sys_rseq_slice_yield
+472	common	fchroot			sys_fchroot
 
 #
 # Due to a historical design error, certain syscalls are numbered differently
diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/syscall.tbl
index a9bca4e484de..d354bb231796 100644
--- a/arch/xtensa/kernel/syscalls/syscall.tbl
+++ b/arch/xtensa/kernel/syscalls/syscall.tbl
@@ -442,3 +442,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index a627acc8fb5f..5b7e77a7c736 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns)
 #define __NR_rseq_slice_yield 471
 __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
 
+/* fs/open.c */
+#define __NR_fchroot 472
+__SYSCALL(__NR_fchroot, sys_fchroot)
+
 #undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 473
 
 /*
  * 32 bit systems traditionally used different
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index 7a42b32b6577..0ab531605120 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -412,3 +412,4 @@
 469	common	file_setattr			sys_file_setattr
 470	common	listns				sys_listns
 471	common	rseq_slice_yield		sys_rseq_slice_yield
+472	common	fchroot				sys_fchroot

-- 
2.53.0


^ permalink raw reply related

* [PATCH RFC 6/7] selftests/filesystems: add failfs selftests
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>

Test the failfs semantics and both new entry points:

- fchdir(FD_FAILFS_ROOT):

  * working directory lookups and getcwd() fail
  * other sentinels are rejected
  * the state is recoverable while the root is untouched

- fchroot() with regular fds:
  * chroot parity
  * CAP_SYS_CHROOT required
  * ENOTDIR/EBADF/EINVAL checks

- fchroot(FD_FAILFS_ROOT):

  * absolute lookups, stat, statfs and opens of the root including O_PATH fail with EOPNOTSUPP
  * dirfd-anchored I/O keeps working
  * ".." walks clamp at the top of the mount tree
  * /proc magic links resolve but can't be stat through
  * absolute symlinks fail while relative symlinks keep resolving

- Unprivileged entry requires no_new_privs and is rejected for
  chrooted callers

- entering makes the task count as chrooted so user namespace creation
  fails

- Nothing can be mounted on top of failfs and OPEN_TREE_CLONE is rejected

- setns() to a kept mount namespace fd restores root and working
  directory

- The failfs root is inherited across fork() and absolute exec fails

- Exec by fd of a dynamically linked binary fails on opening its
  absolute PT_INTERP interpreter

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/Makefile                   |   1 +
 .../selftests/filesystems/failfs/.gitignore        |   2 +
 .../testing/selftests/filesystems/failfs/Makefile  |   5 +
 .../selftests/filesystems/failfs/failfs_test.c     | 506 +++++++++++++++++++++
 4 files changed, 514 insertions(+)

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8d4db2241cc2..f87167bcf582 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -33,6 +33,7 @@ TARGETS += fchmodat2
 TARGETS += filesystems
 TARGETS += filesystems/binderfs
 TARGETS += filesystems/epoll
+TARGETS += filesystems/failfs
 TARGETS += filesystems/fat
 TARGETS += filesystems/overlayfs
 TARGETS += filesystems/statmount
diff --git a/tools/testing/selftests/filesystems/failfs/.gitignore b/tools/testing/selftests/filesystems/failfs/.gitignore
new file mode 100644
index 000000000000..cd3b5d884d7e
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+failfs_test
diff --git a/tools/testing/selftests/filesystems/failfs/Makefile b/tools/testing/selftests/filesystems/failfs/Makefile
new file mode 100644
index 000000000000..3c5d98b4fe72
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+TEST_GEN_PROGS := failfs_test
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/failfs/failfs_test.c b/tools/testing/selftests/filesystems/failfs/failfs_test.c
new file mode 100644
index 000000000000..bb72a3d2102b
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/failfs_test.c
@@ -0,0 +1,506 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <link.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef __NR_fchroot
+#define __NR_fchroot 472
+#endif
+
+#ifndef FD_PIDFS_ROOT
+#define FD_PIDFS_ROOT -10002
+#endif
+
+#ifndef FD_NSFS_ROOT
+#define FD_NSFS_ROOT -10003
+#endif
+
+#ifndef FD_FAILFS_ROOT
+#define FD_FAILFS_ROOT -10004
+#endif
+
+#define NOBODY_UID 65534
+
+static int sys_fchroot(int fd, unsigned int flags)
+{
+	return syscall(__NR_fchroot, fd, flags);
+}
+
+/*
+ * Raw syscall: glibc's getcwd() rejects the kernel's "(unreachable)"
+ * result and falls back to a generic implementation.
+ */
+static long sys_getcwd(char *buf, size_t size)
+{
+	return syscall(__NR_getcwd, buf, size);
+}
+
+static int drop_to_nobody(void)
+{
+	return setresuid(NOBODY_UID, NOBODY_UID, NOBODY_UID);
+}
+
+/* Is fd a dynamically linked ELF with an absolute PT_INTERP interpreter? */
+static int elf_has_absolute_interp(int fd)
+{
+	ElfW(Ehdr) ehdr;
+	ElfW(Phdr) phdr;
+	char interp;
+	int i;
+
+	if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr))
+		return 0;
+	if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0)
+		return 0;
+
+	for (i = 0; i < ehdr.e_phnum; i++) {
+		if (pread(fd, &phdr, sizeof(phdr),
+			  ehdr.e_phoff + i * sizeof(phdr)) != sizeof(phdr))
+			return 0;
+		if (phdr.p_type != PT_INTERP)
+			continue;
+		if (pread(fd, &interp, 1, phdr.p_offset) != 1)
+			return 0;
+		return interp == '/';
+	}
+
+	return 0;
+}
+
+TEST(fchdir_sentinel)
+{
+	char buf[PATH_MAX];
+	int fd;
+
+	ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);
+
+	/* The working directory is unreachable from the process root. */
+	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+	ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0);
+
+	/* Every AT_FDCWD-relative lookup fails. */
+	ASSERT_EQ(openat(AT_FDCWD, "foo", O_RDONLY), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(openat(AT_FDCWD, ".", O_RDONLY), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(openat(AT_FDCWD, "..", O_RDONLY), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(openat(AT_FDCWD, "foo", O_WRONLY | O_CREAT, 0600), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* The cwd cannot be pinned by following /proc/self/cwd into it. */
+	ASSERT_EQ(open("/proc/self/cwd", O_PATH), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* The root is untouched so absolute lookups keep working... */
+	fd = open("/", O_RDONLY | O_DIRECTORY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	/* ... and the working directory can be recovered. */
+	ASSERT_EQ(chdir("/"), 0);
+	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+	ASSERT_EQ(strcmp(buf, "/"), 0);
+}
+
+TEST(fchdir_rejects_other_sentinels)
+{
+	ASSERT_EQ(fchdir(FD_PIDFS_ROOT), -1);
+	ASSERT_EQ(errno, EBADF);
+	ASSERT_EQ(fchdir(FD_NSFS_ROOT), -1);
+	ASSERT_EQ(errno, EBADF);
+	ASSERT_EQ(fchdir(-10009), -1);
+	ASSERT_EQ(errno, EBADF);
+}
+
+TEST(fchroot_flags)
+{
+	int fd;
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 1), -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	fd = open("/", O_PATH | O_DIRECTORY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(sys_fchroot(fd, 1), -1);
+	ASSERT_EQ(errno, EINVAL);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(fchroot_bad_fd)
+{
+	ASSERT_EQ(sys_fchroot(-1, 0), -1);
+	ASSERT_EQ(errno, EBADF);
+
+	/* Only FD_FAILFS_ROOT is a valid sentinel. */
+	ASSERT_EQ(sys_fchroot(FD_PIDFS_ROOT, 0), -1);
+	ASSERT_EQ(errno, EBADF);
+	ASSERT_EQ(sys_fchroot(FD_NSFS_ROOT, 0), -1);
+	ASSERT_EQ(errno, EBADF);
+}
+
+TEST(fchroot_notdir)
+{
+	int fd;
+
+	fd = open("/proc/self/status", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(sys_fchroot(fd, 0), -1);
+	ASSERT_EQ(errno, ENOTDIR);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(fchroot_realfd_requires_cap)
+{
+	int fd;
+
+	if (geteuid() == 0)
+		ASSERT_EQ(drop_to_nobody(), 0);
+
+	fd = open("/", O_PATH | O_DIRECTORY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(sys_fchroot(fd, 0), -1);
+	ASSERT_EQ(errno, EPERM);
+	ASSERT_EQ(close(fd), 0);
+}
+
+TEST(fchroot_realfd)
+{
+	char template[] = "/tmp/failfs_test.XXXXXX";
+	char path[PATH_MAX];
+	struct stat st;
+	int tmpfd, dfd, fd;
+
+	if (geteuid() != 0)
+		SKIP(return, "fchroot() with a regular fd requires CAP_SYS_CHROOT");
+
+	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+	ASSERT_GE(tmpfd, 0);
+
+	ASSERT_NE(mkdtemp(template), NULL);
+	snprintf(path, sizeof(path), "%s/canary", template);
+	fd = open(path, O_WRONLY | O_CREAT, 0600);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	dfd = open(template, O_PATH | O_DIRECTORY);
+	ASSERT_GE(dfd, 0);
+	ASSERT_EQ(sys_fchroot(dfd, 0), 0);
+	ASSERT_EQ(close(dfd), 0);
+
+	ASSERT_EQ(stat("/canary", &st), 0);
+
+	/* Best-effort cleanup: dirfd-anchored I/O works with the new root. */
+	snprintf(path, sizeof(path), "%s/canary", template + strlen("/tmp/"));
+	unlinkat(tmpfd, path, 0);
+	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+}
+
+TEST(fchroot_sentinel)
+{
+	char template[] = "/tmp/failfs_test.XXXXXX";
+	struct stat realroot, st;
+	struct statfs sfs;
+	char buf[PATH_MAX];
+	int procfd, tmpfd, dfd, fd;
+	struct {
+		struct file_handle handle;
+		unsigned char f_handle[MAX_HANDLE_SZ];
+	} fh;
+	int mntid;
+	ssize_t ret;
+
+	if (geteuid() != 0)
+		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+	ASSERT_EQ(stat("/", &realroot), 0);
+	procfd = open("/proc", O_PATH | O_DIRECTORY);
+	ASSERT_GE(procfd, 0);
+	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+	ASSERT_GE(tmpfd, 0);
+	ASSERT_NE(mkdtemp(template), NULL);
+	dfd = open(template, O_RDONLY | O_DIRECTORY);
+	ASSERT_GE(dfd, 0);
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+	/* Absolute lookups fail. */
+	ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(mkdir("/foo", 0700), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/*
+	 * The root cannot be referenced at all - not even an O_PATH open,
+	 * which skips ->permission(), because it lands on the root as a
+	 * jumped walk terminal that ->d_weak_revalidate() refuses.
+	 */
+	ASSERT_EQ(open("/", O_RDONLY | O_DIRECTORY), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(open("/", O_PATH), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+	ASSERT_EQ(statfs("/", &sfs), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/*
+	 * It cannot be pinned by following /proc/self/root into it either
+	 * (only the root is in failfs here, so self/cwd is still real).
+	 */
+	ASSERT_EQ(openat(procfd, "self/root", O_PATH), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* Nor encoded into a file handle. */
+	fh.handle.handle_bytes = MAX_HANDLE_SZ;
+	ASSERT_EQ(name_to_handle_at(AT_FDCWD, "/", &fh.handle, &mntid, 0), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* The working directory is now unreachable from the root. */
+	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+	ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0);
+
+	/* Lookups anchored at real directories keep working. */
+	fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+	fd = openat(dfd, "canary", O_WRONLY | O_CREAT, 0600);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(write(fd, "x", 1), 1);
+	ASSERT_EQ(close(fd), 0);
+	fd = openat(dfd, "canary", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	/* ".." walks clamp at the top of the mount tree, not at failfs. */
+	fd = openat(AT_FDCWD, "../../../../../../../../../..", O_PATH);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(fstat(fd, &st), 0);
+	ASSERT_EQ(st.st_dev, realroot.st_dev);
+	ASSERT_EQ(st.st_ino, realroot.st_ino);
+	ASSERT_EQ(close(fd), 0);
+
+	/* readlink of the magic link still works: it does not follow. */
+	ret = readlinkat(procfd, "self/root", buf, sizeof(buf) - 1);
+	ASSERT_GT(ret, 0);
+	buf[ret] = '\0';
+	TH_LOG("/proc/self/root points to '%s'", buf);
+
+	/* But following it into failfs is refused. */
+	ASSERT_EQ(fstatat(procfd, "self/root", &st, 0), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* Best-effort cleanup via the pre-opened dirfds. */
+	unlinkat(dfd, "canary", 0);
+	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+}
+
+TEST(fchroot_sentinel_absolute_symlink)
+{
+	char template[] = "/tmp/failfs_test.XXXXXX";
+	int tmpfd, dfd, fd;
+
+	if (geteuid() != 0)
+		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+	ASSERT_GE(tmpfd, 0);
+	ASSERT_NE(mkdtemp(template), NULL);
+	dfd = open(template, O_RDONLY | O_DIRECTORY);
+	ASSERT_GE(dfd, 0);
+
+	fd = openat(dfd, "target", O_WRONLY | O_CREAT, 0600);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+	ASSERT_EQ(symlinkat("target", dfd, "rel"), 0);
+	ASSERT_EQ(symlinkat("/etc", dfd, "abs"), 0);
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+	/* Relative symlinks keep resolving within the dirfd-anchored walk... */
+	fd = openat(dfd, "rel", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	/* ... absolute symlinks restart the walk at the failfs root. */
+	ASSERT_EQ(openat(dfd, "abs", O_RDONLY), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* Best-effort cleanup via the pre-opened dirfds. */
+	unlinkat(dfd, "abs", 0);
+	unlinkat(dfd, "rel", 0);
+	unlinkat(dfd, "target", 0);
+	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+}
+
+TEST(fchroot_sentinel_unprivileged)
+{
+	char buf[PATH_MAX];
+
+	if (geteuid() == 0)
+		ASSERT_EQ(drop_to_nobody(), 0);
+
+	/* Without no_new_privs entering failfs is not allowed... */
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
+	ASSERT_EQ(errno, EPERM);
+
+	/* ... with no_new_privs set it is allowed. */
+	ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+	ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* The task counts as chrooted: no user namespaces anymore. */
+	ASSERT_EQ(unshare(CLONE_NEWUSER), -1);
+	ASSERT_EQ(errno, EPERM);
+
+	/* With both root and cwd in failfs getcwd() reports "/". */
+	ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);
+	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+	ASSERT_EQ(strcmp(buf, "/"), 0);
+}
+
+TEST(fchroot_sentinel_rejected_when_chrooted)
+{
+	char template[] = "/tmp/failfs_test.XXXXXX";
+	int tmpfd;
+
+	if (geteuid() != 0)
+		SKIP(return, "chroot() requires CAP_SYS_CHROOT");
+
+	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+	ASSERT_GE(tmpfd, 0);
+	ASSERT_NE(mkdtemp(template), NULL);
+	ASSERT_EQ(chroot(template), 0);
+	ASSERT_EQ(chdir("/"), 0);
+
+	/* Remove the jail while still privileged; sticky /tmp blocks nobody. */
+	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+
+	ASSERT_EQ(drop_to_nobody(), 0);
+	ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+
+	/* An unprivileged chrooted task must not lift its ".." barrier. */
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
+	ASSERT_EQ(errno, EPERM);
+}
+
+TEST(fchroot_sentinel_no_overmount)
+{
+	if (geteuid() != 0)
+		SKIP(return, "mounting requires privileges");
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+	/*
+	 * Nothing can be mounted on top of the failfs root. It cannot even
+	 * be named as a mount target: resolving "/" is refused before the
+	 * mount machinery (which, failfs being in no mount namespace, would
+	 * reject it anyway) is ever reached. open_tree(OPEN_TREE_CLONE) is
+	 * likewise moot since no fd to the root can be obtained.
+	 */
+	ASSERT_EQ(mount("none", "/", "tmpfs", 0, NULL), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+}
+
+TEST(fchroot_sentinel_setns_escape)
+{
+	struct stat realroot, st;
+	int nsfd;
+
+	if (geteuid() != 0)
+		SKIP(return, "setns() to a mount namespace requires privileges");
+
+	ASSERT_EQ(stat("/", &realroot), 0);
+	nsfd = open("/proc/self/ns/mnt", O_RDONLY);
+	ASSERT_GE(nsfd, 0);
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+	ASSERT_EQ(open("/etc", O_PATH), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+
+	/* A mount namespace fd is the key out: it resets root and cwd. */
+	ASSERT_EQ(setns(nsfd, CLONE_NEWNS), 0);
+	ASSERT_EQ(close(nsfd), 0);
+
+	ASSERT_EQ(stat("/", &st), 0);
+	ASSERT_EQ(st.st_dev, realroot.st_dev);
+	ASSERT_EQ(st.st_ino, realroot.st_ino);
+}
+
+TEST(fchroot_sentinel_exec)
+{
+	if (geteuid() != 0)
+		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+	ASSERT_EQ(execl("/bin/true", "true", NULL), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+}
+
+TEST(fchroot_sentinel_exec_interpreter)
+{
+	static const char * const argv[] = { "failfs_test", NULL };
+	static const char * const envp[] = { NULL };
+	int exefd;
+
+	if (geteuid() != 0)
+		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+	/* Exec ourselves: the one binary guaranteed to be around. */
+	exefd = open("/proc/self/exe", O_RDONLY);
+	ASSERT_GE(exefd, 0);
+	if (!elf_has_absolute_interp(exefd))
+		SKIP(return, "test binary has no absolute PT_INTERP interpreter");
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+	/*
+	 * The binary itself needs no path lookup - it is executed by fd -
+	 * but loading it fails on opening the absolute PT_INTERP
+	 * interpreter.
+	 */
+	ASSERT_EQ(syscall(__NR_execveat, exefd, "", argv, envp,
+			  AT_EMPTY_PATH), -1);
+	ASSERT_EQ(errno, EOPNOTSUPP);
+}
+
+TEST(fchroot_sentinel_inherited)
+{
+	pid_t pid;
+	int status;
+
+	if (geteuid() != 0)
+		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		if (open("/etc", O_PATH) != -1 || errno != EOPNOTSUPP)
+			_exit(1);
+		_exit(0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_HARNESS_MAIN

-- 
2.53.0


^ permalink raw reply related

* [PATCH RFC 7/7] Documentation: add failfs documentation
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
  To: linux-fsdevel, Andy Lutomirski, Jann Horn
  Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
	Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>

Document the failfs semantics, the FD_FAILFS_ROOT sentinel, the
fchroot() entry requirements, and the ways back out.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 Documentation/filesystems/failfs.rst | 64 ++++++++++++++++++++++++++++++++++++
 Documentation/filesystems/index.rst  |  1 +
 2 files changed, 65 insertions(+)

diff --git a/Documentation/filesystems/failfs.rst b/Documentation/filesystems/failfs.rst
new file mode 100644
index 000000000000..46d91525916b
--- /dev/null
+++ b/Documentation/filesystems/failfs.rst
@@ -0,0 +1,64 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======
+failfs
+======
+
+failfs is a kernel-internal filesystem that fails every operation
+reaching it with ``EOPNOTSUPP``. It is the counterpart to nullfs. Where
+nullfs is a permanently empty failfs means "nothing is supported here". It
+cannot be mounted from userspace, nothing can be mounted on top of it. It
+cannot be cloned.
+
+The only way into it is the ``FD_FAILFS_ROOT`` file descriptor sentinel which
+is understood by ``fchdir(2)`` and ``fchroot(2)``.
+
+Semantics
+=========
+
+Every path walk of a component through failfs fails with
+``EOPNOTSUPP`` before that component is parsed, including ``.``.
+
+The root itself cannot be opened at all not even with ``O_PATH``.
+
+A process with its working directory in failfs fails every
+``AT_FDCWD``-relative lookup. As with any working directory that is
+unreachable from the process root, the ``getcwd(2)`` system call returns
+a path prefixed with ``(unreachable)``.
+
+A process with its root directory in failfs fails every absolute path
+lookup including absolute symlinks and the interpreter of dynamically
+linked binaries. In other words, this fails exec.
+
+Lookups anchored at explicit directory file descriptors keep working. It
+is the ``fs_struct`` equivalent of ``RESOLVE_BENEATH``. The process must
+anchor every lookup at a file descriptor it explicitly holds.
+
+Entering
+========
+
+``fchroot(FD_FAILFS_ROOT, 0)`` requires ``CAP_SYS_CHROOT`` in the
+caller's user namespace, mirroring ``chroot(2)``. Unprivileged callers
+may enter if both of the following hold:
+
+* ``no_new_privs`` is set: setuid binaries on regular mounts remain
+  reachable via inherited directory file descriptors and executing them
+  with an unusable root directory is the classic confused deputy.
+
+* The caller is not already chrooted: the root directory is what
+  confines ``..`` resolution and the failfs root can never be reached by
+  walking up a real mount tree, so moving the root of a chrooted task to
+  failfs would allow it to escape its chroot via ``openat(fd, "..")``.
+
+Leaving
+=======
+
+A process that entered failfs counts as chrooted. It cannot create user
+namespaces to regain ``CAP_SYS_CHROOT``, and ``chroot(2)`` or
+``fchroot(2)`` back out require ``CAP_SYS_CHROOT``. The only other exit
+is ``setns(2)`` with a mount namespace file descriptor, which requires
+``CAP_SYS_ADMIN`` over the target mount namespace as well as
+``CAP_SYS_CHROOT`` and ``CAP_SYS_ADMIN`` in the caller's user namespace
+and resets both root and working directory. A process that holds no such
+file descriptor and restricts ``*chdir()``/``*chroot()``/``setns()`` via
+seccomp has thrown away the key.
diff --git a/Documentation/filesystems/index.rst b/Documentation/filesystems/index.rst
index 1f71cf159547..734a45e51667 100644
--- a/Documentation/filesystems/index.rst
+++ b/Documentation/filesystems/index.rst
@@ -91,6 +91,7 @@ Documentation for filesystem implementations.
    ext3
    ext4/index
    f2fs
+   failfs
    gfs2/index
    hfs
    hfsplus

-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 0/10] RFC 6724 rule 5.5 support
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest

Hi all,


changes relative to previous version:

- ipv6_saddr_rule5p5() now makes a local copy of the flowi6; I was trying
  to avoid that but it's just an invitation for breakage.
- added check + WARN_ONCE on subtree routes w/ nexthop objects.  The kernel
  doesn't allow adding such routes, this is just a safety guard.
  (this probably fixes the false positive complaint by Sashiko, it's a
  reasonable safety guard to have independent of that.)
- new commit that does adds a comment about this on the kernel reject of
  subtree + nexthop object routes, also reword the error texts to make sense
- added missing _rcu on iterating nexthops
- added missing +x on selftest
- fixed a comment & 2 checkpatch nits
- rephrased 2 parts of the documentation a little bit, actually include it in
  the doc build (from ipv6.rst)
- added a selftest that uses (non-nexthop-object) ECMP routes

remainder is the original cover letter:


this patchset implements RFC6724 rule 5.5.  For the unaquainted:

   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
   If SA or SA's prefix is assigned by the selected next-hop that will
   be used to send to D and SB or SB's prefix is assigned by a different
   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
   assigned by the next-hop that will be used to send to D and SA or
   SA's prefix is assigned by a different next-hop, then prefer SB.

The way this is done is through IPv6 subtree routes.  If a router
advertises some prefix in its RA/PIOs, source specific subtree routes
should be created for the default route (and RIOs) installed as a result
of processing that RA.

This may initially sound like a weird way to do it, but for one RFC8028
requires the subtree routes anyway, and also I did try the more obvious
approaches (explicitly tracking it, putting it on the address, putting
it on the neighbor entry) and all of them break in some scenarios.

I've put together a selftest, there's also a rather hacky test suite
created for an IETF hackathon: https://github.com/eqvinox/rule5p5-tests
(it's not specific to this patchset.)  I've also been dogfooding these
patches on my personal devices for more than a year.

Rule 5.5 itself has extensive history at the IETF, including changing
from optional to mandatory in the recent 6724 update.  It is immensely
useful (really: required) to make multihoming, renumbering and failover
work.

@Jakub you had previously asked me to resubmit the "prep" patches since
it was at a poor time (cf. Fri, Jul 25, 2025 at 05:39:58PM -0700).
(I had tried submitting the preparation bits on its own.)

@Paolo you had looked at the lookup fix:
On Tue, Nov 11, 2025 at 11:13:30AM +0100, Paolo Abeni wrote:
> The patch LGTM, and I agree this should go via net-next, given that it's
> really a corner case and I could miss nasty side-effects.
>
> It looks like you have some testing scenario handy: it would be great to
> include it as a paired self-test; could you please add it?

Cheers,


equi (David)



diffstat:
 Documentation/networking/ipv6-addrsel.rst                |  73 +++++++++++++++++++
 Documentation/networking/ipv6.rst                        |   2 +
 MAINTAINERS                                              |   1 +
 include/net/addrconf.h                                   |   4 +
 include/net/ip6_route.h                                  |  26 -------
 net/ipv4/nexthop.c                                       |   5 +-
 net/ipv6/Kconfig                                         |  18 ++++-
 net/ipv6/addrconf.c                                      | 156 ++++++++++++++++++++++++++++++++++++---
 net/ipv6/ip6_fib.c                                       |   5 +-
 net/ipv6/ip6_output.c                                    |  26 +++++--
 net/ipv6/route.c                                         |  26 +++++--
 tools/testing/selftests/net/Makefile                     |   1 +
 tools/testing/selftests/net/config                       |   1 +
 tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py | 285 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 14 files changed, 574 insertions(+), 55 deletions(-)

^ permalink raw reply

* [PATCH net-next v2 01/10] net/ipv6: fix lookup for ::/0 (non-)subtree route
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

Assume a scenario with something like the following routes:
default via fe80::1 dev dummy0
2001:db8:1::/48 via fe80::10 dev dummy0
2001:db8:1::/48 from 2001:db8:1:2::/64 via fe80::12 dev dummy0

Now if a lookup happens for 2001:db8:1::2345, but with a source address
*not* covered by the third route, the expectation is to hit the second
one.  Unfortunately, this was broken since the code, on failing the
lookup in the subtree, didn't consider the node itself which the subtree
is attached to, i.e. route #2 above.

The fix is simple, check if the subtree is attached to a node that is
itself a valid route before backtracking to less specific destination
prefixes.

This case is somewhat rare for several reasons.  To begin with, subtree
routes are most commonly attached to the default destination.
Additionally, in the rare cases where a non-default destination prefix
is host to subtree routes, the fallback on not hitting any subtree route
is commonly a default route (or a subtree route on that).

(Note that this was working for the "::/0 from ::/0" case since the root
node is special-cased.  The issue was discovered during RFC 6724 rule
5.5 testing, trying to find edge cases.)

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
Cc: Lorenzo Colitti <lorenzo@google.com>
Cc: Maciej Żenczykowski <maze@google.com>
---
 net/ipv6/ip6_fib.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index a130cdfaebfb..273f2bfc5286 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -1656,8 +1656,11 @@ static struct fib6_node *fib6_node_lookup_1(struct fib6_node *root,
 					struct fib6_node *sfn;
 					sfn = fib6_node_lookup_1(subtree,
 								 args + 1);
-					if (!sfn)
+					if (!sfn) {
+						if (fn->fn_flags & RTN_RTINFO)
+							return fn;
 						goto backtrack;
+					}
 					fn = sfn;
 				}
 #endif
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 02/10] net/ipv6: flatten ip6_route_get_saddr
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

Inline ip6_route_get_saddr()'s functionality in rt6_fill_node(), to
prepare for replacing the former with a dst based function.

NB: the l3mdev handling introduced by commit 252442f2ae31 ("ipv6: fix
source address selection with route leak") is dropped here - the l3mdev
ifindex was a constant 0 on this call site, so that code was in fact
dead.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 net/ipv6/route.c | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index a1301334da48..704afe2ddea7 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -5850,9 +5850,19 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 			if (nla_put_u32(skb, RTA_IIF, iif))
 				goto nla_put_failure;
 	} else if (dest) {
-		struct in6_addr saddr_buf;
-		if (ip6_route_get_saddr(net, rt, dest, 0, 0, &saddr_buf) == 0 &&
-		    nla_put_in6_addr(skb, RTA_PREFSRC, &saddr_buf))
+		struct in6_addr saddr_buf, *saddr = NULL;
+
+		if (rt->fib6_prefsrc.plen) {
+			saddr = &rt->fib6_prefsrc.addr;
+		} else {
+			struct net_device *dev = fib6_info_nh_dev(rt);
+
+			if (ipv6_dev_get_saddr(net, dev, dest, 0,
+					       &saddr_buf) == 0)
+				saddr = &saddr_buf;
+		}
+
+		if (saddr && nla_put_in6_addr(skb, RTA_PREFSRC, saddr))
 			goto nla_put_failure;
 	}
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 03/10] net/ipv6: create ipv6_fl_get_saddr
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

This adds passing the relevant flow information as well as selected
nexthop into the source address selection code, to allow the RFC6724
rule 5.5 code to look at its details.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 include/net/addrconf.h |  4 ++++
 net/ipv6/addrconf.c    | 45 +++++++++++++++++++++++++++++++-----------
 2 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/include/net/addrconf.h b/include/net/addrconf.h
index 8ced27a8229b..7bc0711b8717 100644
--- a/include/net/addrconf.h
+++ b/include/net/addrconf.h
@@ -142,6 +142,10 @@ struct inet6_ifaddr *ipv6_get_ifaddr(struct net *net,
 int ipv6_dev_get_saddr(struct net *net, const struct net_device *dev,
 		       const struct in6_addr *daddr, unsigned int srcprefs,
 		       struct in6_addr *saddr);
+int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst,
+		      const struct net_device *dst_dev,
+		      const struct sock *sk, unsigned int srcprefs,
+		      struct flowi6 *fl6);
 int ipv6_get_lladdr(struct net_device *dev, struct in6_addr *addr,
 		    u32 banned_flags);
 bool inet_rcv_saddr_equal(const struct sock *sk, const struct sock *sk2,
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index f1fe9ede1edb..0ba46ed518a9 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1542,7 +1542,9 @@ struct ipv6_saddr_score {
 };
 
 struct ipv6_saddr_dst {
-	const struct in6_addr *addr;
+	const struct flowi6 *fl6;
+	const struct dst_entry *dst;
+	const struct sock *sk;
 	int ifindex;
 	int scope;
 	int label;
@@ -1619,7 +1621,7 @@ static int ipv6_get_saddr_eval(struct net *net,
 		break;
 	case IPV6_SADDR_RULE_LOCAL:
 		/* Rule 1: Prefer same address */
-		ret = ipv6_addr_equal(&score->ifa->addr, dst->addr);
+		ret = ipv6_addr_equal(&score->ifa->addr, &dst->fl6->daddr);
 		break;
 	case IPV6_SADDR_RULE_SCOPE:
 		/* Rule 2: Prefer appropriate scope
@@ -1697,11 +1699,11 @@ static int ipv6_get_saddr_eval(struct net *net,
 		 *	    non-ORCHID vs non-ORCHID
 		 */
 		ret = !(ipv6_addr_orchid(&score->ifa->addr) ^
-			ipv6_addr_orchid(dst->addr));
+			ipv6_addr_orchid(&dst->fl6->daddr));
 		break;
 	case IPV6_SADDR_RULE_PREFIX:
 		/* Rule 8: Use longest matching prefix */
-		ret = ipv6_addr_diff(&score->ifa->addr, dst->addr);
+		ret = ipv6_addr_diff(&score->ifa->addr, &dst->fl6->daddr);
 		if (ret > score->ifa->prefix_len)
 			ret = score->ifa->prefix_len;
 		score->matchlen = ret;
@@ -1819,9 +1821,10 @@ static int ipv6_get_saddr_master(struct net *net,
 	return hiscore_idx;
 }
 
-int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
-		       const struct in6_addr *daddr, unsigned int prefs,
-		       struct in6_addr *saddr)
+int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst_entry,
+		      const struct net_device *dst_dev,
+		      const struct sock *sk, unsigned int prefs,
+		      struct flowi6 *fl6)
 {
 	struct ipv6_saddr_score scores[2], *hiscore;
 	struct ipv6_saddr_dst dst;
@@ -1832,11 +1835,13 @@ int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
 	int hiscore_idx = 0;
 	int ret = 0;
 
-	dst_type = __ipv6_addr_type(daddr);
-	dst.addr = daddr;
+	dst_type = __ipv6_addr_type(&fl6->daddr);
+	dst.fl6 = fl6;
+	dst.sk = sk;
+	dst.dst = dst_entry;
 	dst.ifindex = dst_dev ? dst_dev->ifindex : 0;
 	dst.scope = __ipv6_addr_src_scope(dst_type);
-	dst.label = ipv6_addr_label(net, daddr, dst_type, dst.ifindex);
+	dst.label = ipv6_addr_label(net, &fl6->daddr, dst_type, dst.ifindex);
 	dst.prefs = prefs;
 
 	scores[hiscore_idx].rule = -1;
@@ -1911,11 +1916,29 @@ int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
 	if (!hiscore->ifa)
 		ret = -EADDRNOTAVAIL;
 	else
-		*saddr = hiscore->ifa->addr;
+		fl6->saddr = hiscore->ifa->addr;
 
 	rcu_read_unlock();
 	return ret;
 }
+EXPORT_SYMBOL(ipv6_fl_get_saddr);
+
+int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
+		       const struct in6_addr *daddr, unsigned int prefs,
+		       struct in6_addr *saddr)
+{
+	struct flowi6 fl6;
+	int ret;
+
+	memset(&fl6, 0, sizeof(fl6));
+	fl6.daddr = *daddr;
+
+	ret = ipv6_fl_get_saddr(net, NULL, dst_dev, NULL, prefs, &fl6);
+	if (!ret)
+		*saddr = fl6.saddr;
+
+	return ret;
+}
 EXPORT_SYMBOL(ipv6_dev_get_saddr);
 
 static int __ipv6_get_lladdr(struct inet6_dev *idev, struct in6_addr *addr,
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 04/10] net/ipv6: use ipv6_fl_get_saddr in output
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

Flatten ip6_route_get_saddr() into ip6_dst_lookup_tail (which really
just means handling fib6_prefsrc), and then replace ipv6_dev_get_saddr
with ipv6_fl_get_saddr to pass down the flow information.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 net/ipv6/ip6_output.c | 26 ++++++++++++++++++++------
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
index 368e4fa3b43c..7fda0152945d 100644
--- a/net/ipv6/ip6_output.c
+++ b/net/ipv6/ip6_output.c
@@ -1143,27 +1143,41 @@ static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
 	int flags = 0;
 
 	/* The correct way to handle this would be to do
-	 * ip6_route_get_saddr, and then ip6_route_output; however,
+	 * ipv6_fl_get_saddr, and then ip6_route_output; however,
 	 * the route-specific preferred source forces the
-	 * ip6_route_output call _before_ ip6_route_get_saddr.
+	 * ip6_route_output call _before_ ipv6_fl_get_saddr.
 	 *
 	 * In source specific routing (no src=any default route),
 	 * ip6_route_output will fail given src=any saddr, though, so
 	 * that's why we try it again later.
 	 */
 	if (ipv6_addr_any(&fl6->saddr)) {
+		struct net_device *l3mdev;
+		struct net_device *dev;
 		struct fib6_info *from;
 		struct rt6_info *rt;
+		bool same_vrf;
 
 		*dst = ip6_route_output(net, sk, fl6);
 		rt = (*dst)->error ? NULL : dst_rt6_info(*dst);
 
 		rcu_read_lock();
 		from = rt ? rcu_dereference(rt->from) : NULL;
-		err = ip6_route_get_saddr(net, from, &fl6->daddr,
-					  sk ? READ_ONCE(inet6_sk(sk)->srcprefs) : 0,
-					  fl6->flowi6_l3mdev,
-					  &fl6->saddr);
+
+		l3mdev = dev_get_by_index_rcu(net, fl6->flowi6_l3mdev);
+		if (!from || !from->fib6_prefsrc.plen || l3mdev)
+			dev = from ? fib6_info_nh_dev(from) : NULL;
+		same_vrf = !l3mdev || l3mdev_master_dev_rcu(dev) == l3mdev;
+		if (from && from->fib6_prefsrc.plen && same_vrf) {
+			fl6->saddr = from->fib6_prefsrc.addr;
+			err = 0;
+		} else {
+			err = ipv6_fl_get_saddr(net, *dst,
+						same_vrf ? dev : l3mdev, sk,
+						sk ? READ_ONCE(inet6_sk(sk)->srcprefs) : 0,
+						fl6);
+		}
+
 		rcu_read_unlock();
 
 		if (err)
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 10/10] net: document RFC6724 rule 5.5 implementation
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

RFC6724 rule 5.5 is anything but obvious, especially if trying to do it
well.  (RFC8028 and its errata kinda proves the point.)

This documents what exactly the Linux kernel does for RFC6724 rule 5.5,
especially what the routing table needs to look like for it to work.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 Documentation/networking/ipv6-addrsel.rst | 73 +++++++++++++++++++++++
 Documentation/networking/ipv6.rst         |  2 +
 MAINTAINERS                               |  1 +
 3 files changed, 76 insertions(+)
 create mode 100644 Documentation/networking/ipv6-addrsel.rst

diff --git a/Documentation/networking/ipv6-addrsel.rst b/Documentation/networking/ipv6-addrsel.rst
new file mode 100644
index 000000000000..bfb385796556
--- /dev/null
+++ b/Documentation/networking/ipv6-addrsel.rst
@@ -0,0 +1,73 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+RFC6724 (IPv6 source address selection) rule 5.5 support
+========================================================
+
+RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
+out quite tricky, but also immensely useful in multihoming scenarios.  For
+reference, it says:
+
+::
+
+   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
+   If SA or SA's prefix is assigned by the selected next-hop that will
+   be used to send to D and SB or SB's prefix is assigned by a different
+   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
+   assigned by the next-hop that will be used to send to D and SA or
+   SA's prefix is assigned by a different next-hop, then prefer SB.
+
+The way this works on Linux is as follows:
+
+- prior to any source address selection happening, when receiving a RA, more
+  than the installation of a default route (or ::/128 route) needs to happen:
+  for each PIO, a source-specific (subtree) route is *additionally* installed.
+  The effect of this is that *after* a source address has been selected, one
+  of the routers that advertised it will remain in use (this is *not* RFC 6724
+  related, but rather RFC 8028.)  At the same time, these extra routes serve
+  to remember which router advertised what.  Note the kernel RA handling code
+  does not currently install the needed subtree routes (cf. below).
+
+- per usual, a route lookup for the IPv6 destination address in consideration
+  is done first.  This is passed around in kernel as a dst_entry.
+
+- the source address selection code iterates through the various rules in
+  RFC 6724.
+
+- if/when rule 5.5 is reached, first of all, there is a check if *any* source
+  specific routes exist in the routing table.  If there are none, the entire
+  code for 5.5 is skipped because it cannot have any effect, but is not free
+  to execute (can involve multiple routing lookups.)  **In applications that
+  use a lot of unbound (e.g. UDP) sockets, installing subtree routes should
+  therefore be avoided to not incur this cost on each source address selection
+  pass.**  Alternatively, applications should bind their sockets to a specific
+  source address such that the selection code is never hit.
+
+- if subtree routes do exist, the source address selection code now repeats
+  the routing lookup done before source address selection is entered, except
+  with the source address under consideration filled in.  This lookup will hit
+  the subtree routes that were installed (see first item), giving a fresh
+  dst_entry.  If the new dst_entry matches the original dst_entry, that means
+  the original router has in fact sent RAs with PIOs for this source address,
+  so it is preferred.  Otherwise it is not.
+
+
+There are a few caveats to consider:
+
+- the kernel currently does not create the subtree routes mentioned in the
+  first item.  This is a separate work item.  At the time of writing of this
+  document, some partial/experimental code was done but deferred.  But this can
+  equally well be performed in userspace processing of RAs, e.g. NetworkManager
+  or plain static configuration.  If RA handling is taken over by a userspace
+  component, it needs to happen there in any case.
+
+- since addresses can also be acquired from DHCPv6, even RA/PIO combinations
+  that didn't result in the creation of any addresses (e.g. A=0) should have
+  subtree routes added.  Those routes *may* be relevant for DHCPv6-generated
+  addresses.
+
+- the "announce check" lookup does not backtrack.  Only the destination prefix
+  that provided the "unspecific" (::/128) match is checked for source prefixes
+  to see what routers advertised what.  This means that for e.g. RIOs, subtree
+  routes also have to be created.  (Backtracking for this case would further
+  increase the cost of source address selection, for a pretty rare situation
+  that has an easy fix/workaround.)
diff --git a/Documentation/networking/ipv6.rst b/Documentation/networking/ipv6.rst
index ba09c2f2dcc7..6a50386ae36e 100644
--- a/Documentation/networking/ipv6.rst
+++ b/Documentation/networking/ipv6.rst
@@ -76,3 +76,5 @@ disable_ipv6
 
 		No IPv6 addresses will be added to interfaces.
 
+
+.. include:: ipv6-addrsel.rst
diff --git a/MAINTAINERS b/MAINTAINERS
index 3fb962e917c2..f9e15b3bf939 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18929,6 +18929,7 @@ F:	Documentation/netlink/specs/rt-addr.yaml
 F:	Documentation/netlink/specs/rt-neigh.yaml
 F:	Documentation/netlink/specs/rt-route.yaml
 F:	Documentation/netlink/specs/rt-rule.yaml
+F:	Documentation/networking/ipv6-addrsel.rst
 F:	include/linux/inetdevice.h
 F:	include/linux/mroute*
 F:	include/net/addrconf.h
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 05/10] net/ipv6: drop ip6_route_get_saddr
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

It's no longer used anywhere.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 include/net/ip6_route.h | 26 --------------------------
 1 file changed, 26 deletions(-)

diff --git a/include/net/ip6_route.h b/include/net/ip6_route.h
index 09ffe0f13ce7..fe13cb1d3257 100644
--- a/include/net/ip6_route.h
+++ b/include/net/ip6_route.h
@@ -140,32 +140,6 @@ void rt6_flush_exceptions(struct fib6_info *f6i);
 void rt6_age_exceptions(struct fib6_info *f6i, struct fib6_gc_args *gc_args,
 			unsigned long now);
 
-static inline int ip6_route_get_saddr(struct net *net, struct fib6_info *f6i,
-				      const struct in6_addr *daddr,
-				      unsigned int prefs, int l3mdev_index,
-				      struct in6_addr *saddr)
-{
-	struct net_device *l3mdev;
-	struct net_device *dev;
-	bool same_vrf;
-	int err = 0;
-
-	rcu_read_lock();
-
-	l3mdev = dev_get_by_index_rcu(net, l3mdev_index);
-	if (!f6i || !f6i->fib6_prefsrc.plen || l3mdev)
-		dev = f6i ? fib6_info_nh_dev(f6i) : NULL;
-	same_vrf = !l3mdev || l3mdev_master_dev_rcu(dev) == l3mdev;
-	if (f6i && f6i->fib6_prefsrc.plen && same_vrf)
-		*saddr = f6i->fib6_prefsrc.addr;
-	else
-		err = ipv6_dev_get_saddr(net, same_vrf ? dev : l3mdev, daddr, prefs, saddr);
-
-	rcu_read_unlock();
-
-	return err;
-}
-
 struct rt6_info *rt6_lookup(struct net *net, const struct in6_addr *daddr,
 			    const struct in6_addr *saddr, int oif,
 			    const struct sk_buff *skb, int flags);
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 06/10] net/ipv6: flip IPV6_SUBTREES default to Y
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

It's needed for RFC6724 rule 5.5, which as it turns out is more
important than was initially discernible.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 net/ipv6/Kconfig | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/net/ipv6/Kconfig b/net/ipv6/Kconfig
index c3806c6ac96f..39659e9b53a3 100644
--- a/net/ipv6/Kconfig
+++ b/net/ipv6/Kconfig
@@ -243,16 +243,26 @@ config IPV6_MULTIPLE_TABLES
 config IPV6_SUBTREES
 	bool "IPv6: source address based routing"
 	depends on IPV6_MULTIPLE_TABLES
+	default y
 	help
 	  Enable routing by source address or prefix.
 
 	  The destination address is still the primary routing key, so mixing
 	  normal and source prefix specific routes in the same routing table
-	  may sometimes lead to unintended routing behavior.  This can be
-	  avoided by defining different routing tables for the normal and
-	  source prefix specific routes.
+	  may sometimes lead to unintended routing behavior if the order of
+	  lookups is misunderstood.  To achieve full separation based on source
+	  address, different routing tables should be defined for each source
+	  prefix.
 
-	  If unsure, say N.
+	  Subtrees specifically address scenarios where local connectivity
+	  between source prefixes is shared and unrestricted, but some
+	  destinations (e.g. the default route) have more fine-grained rules.
+
+	  This feature is required to correctly support RFC6724 rule 5.5 in
+	  source address selection.  Rule 5.5 is a necessity for good
+	  multihoming, renumbering and source prefix liveliness checks.
+
+	  If unsure, say Y.
 
 config IPV6_MROUTE
 	bool "IPv6: multicast routing"
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 07/10] net/ipv6: support RFC6724 rule 5.5 via subtrees
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter, Jen Linkova
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

RFC6724 says:
   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
   If SA or SA's prefix is assigned by the selected next-hop that will
   be used to send to D and SB or SB's prefix is assigned by a different
   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
   assigned by the next-hop that will be used to send to D and SA or
   SA's prefix is assigned by a different next-hop, then prefer SB.

As it turns out, this behavior is immensely useful in handling
renumbering, multihoming, and source address liveliness checks.

That said, just implementing this as an one-shot in source address
selection is not particularly useful if output routing then meanders
between multiple nexthops.  To actually make this work (and work well),
installing source-specific routes for prefixes seen in PIOs is a good
way to go.

To actually select a source address, there's still the route lookup with
an empty source as before, which will select some particular nexthop.
Now, for the various candidate source addresses, this does another
lookup and checks if we got the same nexthop, and prefers those source
addresses - rule 5.5 behavior.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
Cc: Lorenzo Colitti <lorenzo@google.com>
Cc: Maciej Żenczykowski <maze@google.com>
Cc: Jen Linkova <furry13@gmail.com>
---
 net/ipv6/addrconf.c | 113 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 112 insertions(+), 1 deletion(-)

diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index 0ba46ed518a9..fcf5a7d0c37d 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1522,6 +1522,7 @@ enum {
 	IPV6_SADDR_RULE_HOA,
 #endif
 	IPV6_SADDR_RULE_OIF,
+	IPV6_SADDR_RULE_PIO_BY_NEXTHOP,
 	IPV6_SADDR_RULE_LABEL,
 	IPV6_SADDR_RULE_PRIVACY,
 	IPV6_SADDR_RULE_ORCHID,
@@ -1542,13 +1543,14 @@ struct ipv6_saddr_score {
 };
 
 struct ipv6_saddr_dst {
-	const struct flowi6 *fl6;
+	struct flowi6 *fl6;
 	const struct dst_entry *dst;
 	const struct sock *sk;
 	int ifindex;
 	int scope;
 	int label;
 	unsigned int prefs;
+	struct net *net;
 };
 
 static inline int ipv6_saddr_preferred(int type)
@@ -1593,6 +1595,92 @@ static bool ipv6_allow_optimistic_dad(const struct net *net,
 #endif
 }
 
+/* "source address is preferable if the chosen nexthop advertised it as a PIO"
+ * => consider 'advertised as a PIO' to be 'the routes for the source's subtree
+ *    include one with the same nexthop'
+ *
+ * NB: there is no backtracking in the subtree here, this is intentional -
+ * each prefix seen (and accepted) in PIOs creates essentially a "zone" which
+ * is our search scope.
+ */
+static int ipv6_saddr_rule5p5(struct ipv6_saddr_score *score,
+			      struct ipv6_saddr_dst *saddr_dst)
+{
+	const struct rt6_info *rt, *cmp_rt;
+	struct dst_entry *cmp_dst;
+	struct fib6_info *f6i;
+	struct flowi6 fl6;
+	int ret = 0;
+
+	rt = container_of(saddr_dst->dst, struct rt6_info, dst);
+
+	/* fl6->saddr is ::, cf. check at the top of ipv6_fl_get_saddr()
+	 * unfortunately, ip6_route_output can update iif/oif - not making a
+	 * local copy would be rather fragile (considering future changes too)
+	 */
+	fl6 = *saddr_dst->fl6;
+	fl6.saddr = score->ifa->addr;
+	cmp_dst = ip6_route_output(saddr_dst->net, saddr_dst->sk, &fl6);
+
+	if (cmp_dst->error)
+		goto out_release_dst;
+
+	cmp_rt = container_of(cmp_dst, struct rt6_info, dst);
+
+	/* this must work if _any_ nexthop matches; the non-subtree best may
+	 * not be in same order as subtree best
+	 */
+	for (f6i = rcu_dereference(cmp_rt->from); f6i;
+	     f6i = rcu_dereference(f6i->fib6_next)) {
+		struct fib6_nh *f6n;
+		struct fib6_info *sibling;
+
+		/* non-subtree route: says nothing about router advertising this source */
+		if (f6i->fib6_src.plen == 0)
+			continue;
+
+		if (unlikely(f6i->nh)) {
+			/* kernel refuses subtree routes with nexthop objects,
+			 * this should be impossible to hit
+			 */
+			WARN_ONCE(1, "IPv6 dst-src route with nexthop object");
+			continue;
+		}
+
+		f6n = f6i->fib6_nh;
+		if (f6n->nh_common.nhc_dev != saddr_dst->dst->dev ||
+		    f6n->nh_common.nhc_gw_family != AF_INET6)
+			continue;
+
+		if (ipv6_addr_equal(&f6n->nh_common.nhc_gw.ipv6,
+				    &rt->rt6i_gateway)) {
+			ret = 1;
+			goto out_release_dst;
+		}
+
+		if (!f6i->fib6_nsiblings)
+			continue;
+
+		list_for_each_entry_rcu(sibling, &f6i->fib6_siblings, fib6_siblings) {
+			f6n = sibling->fib6_nh;
+
+			if (f6n->nh_common.nhc_dev != saddr_dst->dst->dev ||
+			    f6n->nh_common.nhc_gw_family != AF_INET6)
+				continue;
+
+			if (ipv6_addr_equal(&f6n->nh_common.nhc_gw.ipv6,
+					    &rt->rt6i_gateway)) {
+				ret = 1;
+				goto out_release_dst;
+			}
+		}
+	}
+
+out_release_dst:
+	dst_release(cmp_dst);
+	return ret;
+}
+
 static int ipv6_get_saddr_eval(struct net *net,
 			       struct ipv6_saddr_score *score,
 			       struct ipv6_saddr_dst *dst,
@@ -1677,6 +1765,24 @@ static int ipv6_get_saddr_eval(struct net *net,
 		ret = (!dst->ifindex ||
 		       dst->ifindex == score->ifa->idev->dev->ifindex);
 		break;
+	case IPV6_SADDR_RULE_PIO_BY_NEXTHOP:
+		/* Rule 5.5: Prefer sources advertised by chosen next-hop */
+
+		/* Without subtrees, the source address will make no difference
+		 * in the ip6_route_output call in rule5p5.  Therefore the rule
+		 * 5.5 check becomes useless.  This wouldn't result in any
+		 * errors, but ip6_route_output isn't free, so if subtrees are
+		 * disabled save some cycles by skipping this entirely.
+		 *
+		 * This is done through subtrees_enabled to have the code
+		 * compiled regardless.
+		 */
+		if (fib6_routes_require_src(net) && dst->dst &&
+		    !dst->dst->error)
+			ret = ipv6_saddr_rule5p5(score, dst);
+		else
+			ret = 1;
+		break;
 	case IPV6_SADDR_RULE_LABEL:
 		/* Rule 6: Prefer matching label */
 		ret = ipv6_addr_label(net,
@@ -1835,7 +1941,12 @@ int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst_entry,
 	int hiscore_idx = 0;
 	int ret = 0;
 
+	/* we should never end up here with a non-empty saddr. */
+	if (WARN_ON_ONCE(!ipv6_addr_any(&fl6->saddr)))
+		return 0;
+
 	dst_type = __ipv6_addr_type(&fl6->daddr);
+	dst.net = net;
 	dst.fl6 = fl6;
 	dst.sk = sk;
 	dst.dst = dst_entry;
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 08/10] net/ipv6: leave a note on subtree/nexthop objects
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

The rule 5.5 code doesn't handle nexthop objects, it doesn't need to
since they're currently rejected by the kernel.  There's no fundamental
reason why nexthop objects can't be supported on subtree / dst-src
routes, so leave a note in case somebody wants to do this at a future
point in time.  (NB: I have no idea what else would need to be addressed
for that, hence the "among other things" in the comment.)

The error messages on this reject were also just plain worded wrong,
source-specific routing and source routing both exist and are very
different things, and the source address on a route is yet another
distinct thing.  Admittedly, "source routing" is sometimes (mis)used to
mean source-specific / policy routing, either way let's not add to the
confusion.

For reference:
* source-specific routing: include source address in lookup *after*
  destination, aka subtree routes, aka dst-src routing, aka SADR
  (NB: policy routing / ip rule is source *before* destination)
* source routing: pre-deciding multiple hops / the path through the
  network on ingress, e.g. MPLS, segment routing
* source attribute on a route: preference to apply when selecting a
  source address for outbound connections/packets from this system

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 net/ipv4/nexthop.c |  5 ++++-
 net/ipv6/route.c   | 10 +++++++---
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/net/ipv4/nexthop.c b/net/ipv4/nexthop.c
index 6205bd57aa85..f26ba66954cc 100644
--- a/net/ipv4/nexthop.c
+++ b/net/ipv4/nexthop.c
@@ -1542,8 +1542,11 @@ EXPORT_SYMBOL_GPL(nexthop_for_each_fib6_nh);
 static int check_src_addr(const struct in6_addr *saddr,
 			  struct netlink_ext_ack *extack)
 {
+	/* among other things, ipv6_saddr_rule5p5() would need fixing for this
+	 * (it currently doesn't handle nexthop objects / groups)
+	 */
 	if (!ipv6_addr_any(saddr)) {
-		NL_SET_ERR_MSG(extack, "IPv6 routes using source address can not use nexthop objects");
+		NL_SET_ERR_MSG(extack, "IPv6 dst-src routes (with a source prefix) cannot use nexthop objects");
 		return -EINVAL;
 	}
 	return 0;
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 704afe2ddea7..3323cda37589 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -3786,18 +3786,22 @@ static int fib6_config_validate(struct fib6_config *cfg,
 
 #ifdef CONFIG_IPV6_SUBTREES
 	if (cfg->fc_src_len > 128) {
-		NL_SET_ERR_MSG(extack, "Invalid source address length");
+		NL_SET_ERR_MSG(extack, "Invalid source prefix length");
 		goto errout;
 	}
 
+	/* among other things, ipv6_saddr_rule5p5() would need fixing for this
+	 * (it currently doesn't handle nexthop objects / groups)
+	 */
 	if (cfg->fc_nh_id && cfg->fc_src_len) {
-		NL_SET_ERR_MSG(extack, "Nexthops can not be used with source routing");
+		NL_SET_ERR_MSG(extack,
+			       "IPv6 dst-src routes (with a source prefix) cannot use nexthop objects");
 		goto errout;
 	}
 #else
 	if (cfg->fc_src_len) {
 		NL_SET_ERR_MSG(extack,
-			       "Specifying source address requires IPV6_SUBTREES to be enabled");
+			       "Specifying source prefix requires IPV6_SUBTREES to be enabled");
 		goto errout;
 	}
 #endif
-- 
2.53.0


^ permalink raw reply related

* [PATCH net-next v2 09/10] selftests: net: RFC6724 rule 5.5 tests
From: David 'equinox' Lamparter @ 2026-07-23 12:19 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Simon Horman
  Cc: Ido Schimmel, David Ahern, David S. Miller, Eric Dumazet,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260723122106.99772-1-equinox@diac24.net>

This throws a couple of situations at the IPv6 source address selection
logic, primarily to check that RFC6724 rule 5.5 is applied as expected.

This requires `CONFIG_IPV6_SUBTREES=y`, so that's added to
selftests/net/config.  (The patchset also changes the default on that,
but arguably better to be explicit.)

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 tools/testing/selftests/net/Makefile          |   1 +
 tools/testing/selftests/net/config            |   1 +
 .../net/ipv6_saddr_rfc6724rule5p5.py          | 285 ++++++++++++++++++
 3 files changed, 287 insertions(+)
 create mode 100755 tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py

diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index ab890e6f79dd..07872f838c50 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -55,6 +55,7 @@ TEST_PROGS := \
 	ipv6_flowlabel.sh \
 	ipv6_force_forwarding.sh \
 	ipv6_route_update_soft_lockup.sh \
+	ipv6_saddr_rfc6724rule5p5.py \
 	ipvtap_test.sh \
 	l2_tos_ttl_inherit.sh \
 	l2tp.sh \
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index 96fffca6547c..cc9c0f38a9d1 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -50,6 +50,7 @@ CONFIG_IPV6_ROUTER_PREF=y
 CONFIG_IPV6_RPL_LWTUNNEL=y
 CONFIG_IPV6_SEG6_LWTUNNEL=y
 CONFIG_IPV6_SIT=y
+CONFIG_IPV6_SUBTREES=y
 CONFIG_IPV6_VTI=y
 CONFIG_IPVLAN=m
 CONFIG_IPVTAP=m
diff --git a/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py b/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
new file mode 100755
index 000000000000..21dfffc750ad
--- /dev/null
+++ b/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
@@ -0,0 +1,285 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2023-2026  David 'equinox' Lamparter
+"""
+RFC 6724 (IPv6 source address selection) rule 5.5 tests
+
+For reference:
+
+   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
+   If SA or SA's prefix is assigned by the selected next-hop that will
+   be used to send to D and SB or SB's prefix is assigned by a different
+   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
+   assigned by the next-hop that will be used to send to D and SA or
+   SA's prefix is assigned by a different next-hop, then prefer SB.
+
+(and since it provides the "counterpoint":)
+
+   Rule 8: Use longest matching prefix.
+   If CommonPrefixLen(SA, D) > CommonPrefixLen(SB, D), then prefer SA.
+   Similarly, if CommonPrefixLen(SB, D) > CommonPrefixLen(SA, D), then
+   prefer SB.
+
+Note rule 5.5 was originally optional but made mandatory by
+draft-ietf-6man-rfc6724-update (which at the point of creation of this test
+was already "done" at the IETF but waiting in the RFC editor queue due to a
+blocking dependency.)
+"""
+
+from socket import socket, AF_INET6, SOCK_DGRAM
+from functools import wraps
+from typing import Callable
+
+from lib.py import ksft_run, ksft_exit, ksft_eq
+from lib.py import NetNS, NetNSEnter
+from lib.py import ip
+
+
+def select_addr(dest):
+    """
+    connect() + getsockname() to figure out what was selected as source address
+    """
+    sock = socket(AF_INET6, SOCK_DGRAM, 0)
+    sock.connect((dest, 12345))
+    return sock.getsockname()[0]
+
+
+def in_netns(func: Callable[[], None]) -> Callable[[], None]:
+    """
+    python decorator to put test function in netns
+    """
+
+    @wraps(func)
+    def wrapped() -> None:
+        with NetNS() as testns:
+            with NetNSEnter(str(testns)):
+                func()
+
+    return wrapped
+
+
+@in_netns
+def test_basic() -> None:
+    """
+    Simple & most common case for RFC6724 rule 5.5: multiple default routes
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64   dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default via fe80::1 dev veth0 metric 100")
+    ip("-6 route add default via fe80::2 dev veth0 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+    ip("-6 route del default via fe80::1 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_nh_obj() -> None:
+    """
+    Same as above, but with nexthop objects for the default route
+
+    NB: The kernel doesn't currently allow nexthop objects for subtree routes.
+    """
+
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64   dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+
+    # distinct nexthop objects are used, because what matters is the nexthop
+    # itself, not the nexthop object.  To cover everything, make a group.
+    ip("nexthop add id 101 via fe80::1 dev veth0")
+    ip("nexthop add id 201 group 101")
+    ip("nexthop add id 102 via fe80::2 dev veth0")
+    ip("nexthop add id 202 group 102")
+
+    ip("-6 route add default nhid 201 metric 100")
+    ip("-6 route add default nhid 202 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+    ip("-6 route del default nhid 201 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_low_metric() -> None:
+    """
+    Check that subtree routes take effect even if they are higher metric
+
+    For checking that "router advertised prefix", metric is irrelevant.  It
+    matters for the initial unspecific lookup to find a nexthop to begin with.
+    (The later source address check lookup doesn't change the nexthop, i.e.
+    the effects of metrics are already done.)
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64   dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default via fe80::1 dev veth0 metric 100")
+    ip("-6 route add default via fe80::2 dev veth0 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0 metric 1000")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::3 dev veth0 metric 50")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+    ip("-6 route del default via fe80::1 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_no_subtree() -> None:
+    """
+    Ensure that matching on a non-subtree route doesn't trigger rule 5.5
+
+    (This was non-obviously broken in earlier versions of the implementations,
+    a non-subtree route would still match.  Make sure it doesn't break again.)
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default via fe80::1 dev veth0 metric 100")
+    ip("-6 route add default via fe80::2 dev veth0 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48 via fe80::1 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(
+        select_addr("2001:db8:1001::"),
+        "2001:db8:10::1",
+        "rule 5.5 > rule 8, ignoring non-SADR",
+    )
+
+
+@in_netns
+def test_longer() -> None:
+    """
+    Check functionality for non-default destination.
+
+    This is expected to be very rare in actual practice, and doesn't do
+    backtracking (also refer to kernel docs.)
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default           via fe80::1 dev veth0 metric 100")
+    ip("-6 route add 2001:db8:500::/48 via fe80::2 dev veth0 metric 100")
+    ip("-6 route add default           from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add 2001:db8:500::/48 from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:500::"), "2001:db8:1000::1", "rule 5.5")
+    ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:1000::1", "rule 5.5")
+
+    ip("-6 route add 2001:db8:500:aaa::/64 via fe80::2 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:500::"), "2001:db8:1000::1", "rule 5.5")
+    ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:10::1", "no backtracking")
+
+    ip("-6 route del 2001:db8:500::/48 from 2001:db8:1000::/48 via fe80::2 dev veth0")
+    ip("-6 route add default           from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:500::"), "2001:db8:10::1", "no backtracking")
+    ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:10::1", "no backtracking")
+
+
+@in_netns
+def test_multipath() -> None:
+    """
+    Test with an ECMP (but non-nexthop-object) dst-src route
+
+    Multiple nexthops for the non-subtree route can't make a difference since
+    the rule 5.5 code is called with a dst, not a route.  Nexthop objects on
+    subtree routes are rejected on (attempted) install by the kernel.
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64   dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default via fe80::1 dev veth0 metric 100")
+    ip("-6 route add default via fe80::2 dev veth0 metric 200")
+    ip(
+        "-6 route add default from 2001:db8:10::/48"
+        + " nexthop via fe80::1 dev veth0"
+        + " nexthop via fe80::101 dev veth0"
+    )
+    ip(
+        "-6 route add default from 2001:db8:1000::/48"
+        + " nexthop via fe80::2 dev veth0"
+        + " nexthop via fe80::102 dev veth0"
+    )
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+    # flip order
+    ip(
+        "-6 route replace default from 2001:db8:10::/48"
+        + " nexthop via fe80::101 dev veth0"
+        + " nexthop via fe80::1 dev veth0"
+    )
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "reorder")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "reorder, 5.5 > 8")
+
+    # try the other one
+    ip("-6 route replace default via fe80::101 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "other NH")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "other NH, 5.5 > 8")
+
+    ip("-6 route del default via fe80::101 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "delete/baseline")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "delete/5.5 > 8")
+
+
+def main() -> None:
+    """
+    RFC6724 rule 5.5 test driver
+    """
+    ksft_run(
+        [
+            test_basic,
+            test_nh_obj,
+            test_low_metric,
+            test_no_subtree,
+            test_longer,
+            test_multipath,
+        ]
+    )
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v12 02/29] arm64/fpsimd: Update FA64 and ZT0 enables when loading SME state
From: Mark Rutland @ 2026-07-23 12:30 UTC (permalink / raw)
  To: Mark Brown
  Cc: Marc Zyngier, Joey Gouly, Catalin Marinas, Suzuki K Poulose,
	Will Deacon, Paolo Bonzini, Jonathan Corbet, Shuah Khan,
	Oliver Upton, Dave Martin, Fuad Tabba, Ben Horgan,
	Jean-Philippe Brucker, linux-arm-kernel, kvmarm, linux-kernel,
	kvm, linux-doc, linux-kselftest, Peter Maydell, Eric Auger
In-Reply-To: <3c76650f-ddda-47ae-a676-86016d8e39a6@sirena.org.uk>

On Sat, Jul 18, 2026 at 01:35:21AM +0100, Mark Brown wrote:
> On Thu, Jul 16, 2026 at 04:28:02PM +0100, Mark Brown wrote:
> > On Thu, Jul 16, 2026 at 11:52:36AM +0100, Mark Rutland wrote:
> 
> > > >  		if (test_thread_flag(TIF_SME)) {
> > > > -			unsigned long vq = sve_vq_from_vl(sme_vl);
> > > > -			sysreg_clear_set_s(SYS_SMCR_EL1, SMCR_ELx_LEN, vq - 1);
> > > > +			sysreg_cond_update_s(SYS_SMCR_EL1, task_smcr(current));
> > > > +			isb();
> > > >  		}
> 
> > > What's the ISB for? That mysteriously appeared in v11 without
> > > explanation. It wasn't in the original code, prior versions of the
> > > series, or my suggested rework with the task_smcr() helper.
> 
> > > I don't believe it's necessary to add an ISB here.
> 
> > I can't remember or figure it out, I'll delete.
> 
> Actually I remembered: while SCMR_EL1.LEN is self synchronising the
> "without the need for explict synchronization" wording is not present
> for SMCR_EL1.{FA64,EZT0} and this is no longer explicitly just an update
> of LEN.  It's possible I'm being overly paranoid here, I'll leave the
> isb() and add a comment for the next version.

Ok.

Given we've already forgotten that once so far, that definitely needs a
comment.

Mark.

^ permalink raw reply

* [PATCH] docs: accounting/psi: document /proc/pressure/irq and irq.pressure
From: Tao Cui @ 2026-07-23 12:36 UTC (permalink / raw)
  To: Johannes Weiner, Suren Baghdasaryan
  Cc: linux-doc, Peter Zijlstra, Jonathan Corbet, Tao Cui, linux-kernel,
	cui.tao

From: Tao Cui <cuitao@kylinos.cn>

psi.rst lists only cpu, memory and io, omitting the IRQ/SOFTIRQ pressure
exposed as /proc/pressure/irq (and irq.pressure per cgroup) under
CONFIG_IRQ_TIME_ACCOUNTING. admin-guide/cgroup-v2.rst already documents
irq.pressure; bring psi.rst in line.

IRQ pressure is tracked only for the "full" state: there is no "some"
line, and writing a "some" trigger returns EINVAL.

Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
 Documentation/accounting/psi.rst | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/Documentation/accounting/psi.rst b/Documentation/accounting/psi.rst
index d455db3e5808..6d761625529d 100644
--- a/Documentation/accounting/psi.rst
+++ b/Documentation/accounting/psi.rst
@@ -35,7 +35,8 @@ Pressure interface
 ==================
 
 Pressure information for each resource is exported through the
-respective file in /proc/pressure/ -- cpu, memory, and io.
+respective file in /proc/pressure/ -- cpu, memory, io and, when the
+kernel is built with CONFIG_IRQ_TIME_ACCOUNTING, irq.
 
 The format is as such::
 
@@ -57,6 +58,12 @@ stall state is tracked separately and exported in the "full" averages.
 CPU full is undefined at the system level, but has been reported
 since 5.13, so it is set to zero for backward compatibility.
 
+Unlike the other resources, IRQ pressure is tracked only for the
+"full" state, which accounts for CPU time consumed by interrupt
+handling and therefore unavailable to tasks. The "some" state is not
+reported, and writing a "some" trigger to /proc/pressure/irq fails
+with EINVAL.
+
 The ratios (in %) are tracked as recent trends over ten, sixty, and
 three hundred second windows, which gives insight into short term events
 as well as medium and long term trends. The total absolute stall time
@@ -181,8 +188,9 @@ Cgroup2 interface
 In a system with a CONFIG_CGROUPS=y kernel and the cgroup2 filesystem
 mounted, pressure stall information is also tracked for tasks grouped
 into cgroups. Each subdirectory in the cgroupfs mountpoint contains
-cpu.pressure, memory.pressure, and io.pressure files; the format is
-the same as the /proc/pressure/ files.
+cpu.pressure, memory.pressure, io.pressure and, with
+CONFIG_IRQ_TIME_ACCOUNTING, irq.pressure files; the format is the same
+as the /proc/pressure/ files.
 
 Per-cgroup psi monitors can be specified and used the same way as
 system-wide ones.
-- 
2.43.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox