Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/stable/linux
Language: C
File: drivers/gpu/drm/nouveau/nouveau_prime.c
Function: nouveau_gem_prime_import_sg_table

Call path: nouveau_gem_prime_import_sg_table (drivers/gpu/drm/nouveau/nouveau_prime.c) → nouveau_bo_init (drivers/gpu/drm/nouveau/nouveau_bo.c) → ttm_bo_init (ttm_bo.c) → nouveau_bo_del_ttm (drivers/gpu/drm/nouveau/nouveau_bo.c) → nouveau_bo_ref (drivers/gpu/drm/nouveau/nouveau_bo.h)

### Primary Function

```c
struct drm_gem_object *nouveau_gem_prime_import_sg_table(struct drm_device *dev,
						 struct dma_buf_attachment *attach,
						 struct sg_table *sg)
{
	struct nouveau_drm *drm = nouveau_drm(dev);
	struct drm_gem_object *obj;
	struct nouveau_bo *nvbo;
	struct dma_resv *robj = attach->dmabuf->resv;
	u64 size = attach->dmabuf->size;
	u32 flags = 0;
	int align = 0;
	int ret;

	flags = TTM_PL_FLAG_TT;

	dma_resv_lock(robj, NULL);
	nvbo = nouveau_bo_alloc(&drm->client, &size, &align, flags, 0, 0);
	if (IS_ERR(nvbo)) {
		obj = ERR_CAST(nvbo);
		goto unlock;
	}

	nvbo->valid_domains = NOUVEAU_GEM_DOMAIN_GART;

	/* Initialize the embedded gem-object. We return a single gem-reference
	 * to the caller, instead of a normal nouveau_bo ttm reference. */
	ret = drm_gem_object_init(dev, &nvbo->bo.base, size);
	if (ret) {
		nouveau_bo_ref(NULL, &nvbo);
		obj = ERR_PTR(-ENOMEM);
		goto unlock;
	}

	ret = nouveau_bo_init(nvbo, size, align, flags, sg, robj);
	if (ret) {
		nouveau_bo_ref(NULL, &nvbo);
		obj = ERR_PTR(ret);
		goto unlock;
	}

	obj = &nvbo->bo.base;

unlock:
	dma_resv_unlock(robj);
	return obj;
}
```

### Cross-File Context

[nouveau_bo_ref — sink — drivers/gpu/drm/nouveau/nouveau_bo.h:50-69]
```c
static inline int
nouveau_bo_ref(struct nouveau_bo *ref, struct nouveau_bo **pnvbo)
{
	struct nouveau_bo *prev;

	if (!pnvbo)
		return -EINVAL;
	prev = *pnvbo;

	if (ref) {
		ttm_bo_get(&ref->bo);
		*pnvbo = nouveau_bo(&ref->bo);
	} else {
		*pnvbo = NULL;
	}
	if (prev)
		ttm_bo_put(&prev->bo);

	return 0;
}
```

[nouveau_bo_init — callee — drivers/gpu/drm/nouveau/nouveau_bo.c:295-317]
```c
int
nouveau_bo_init(struct nouveau_bo *nvbo, u64 size, int align, u32 flags,
		 struct sg_table *sg, struct dma_resv *robj)
{
	int type = sg ? ttm_bo_type_sg : ttm_bo_type_device;
	size_t acc_size;
	int ret;

	acc_size = ttm_bo_dma_acc_size(nvbo->bo.bdev, size, sizeof(*nvbo));

	nvbo->bo.mem.num_pages = size >> PAGE_SHIFT;
	nouveau_bo_placement_set(nvbo, flags, 0);

	ret = ttm_bo_init(nvbo->bo.bdev, &nvbo->bo, size, type,
			  &nvbo->placement, align >> PAGE_SHIFT, false,
			  acc_size, sg, robj, nouveau_bo_del_ttm);
	if (ret) {
		/* ttm will call nouveau_bo_del_ttm if it fails.. */
		return ret;
	}

	return 0;
}
```

[nouveau_bo_del_ttm — callee — drivers/gpu/drm/nouveau/nouveau_bo.c:132-150]
```c
static void
nouveau_bo_del_ttm(struct ttm_buffer_object *bo)
{
	struct nouveau_drm *drm = nouveau_bdev(bo->bdev);
	struct drm_device *dev = drm->dev;
	struct nouveau_bo *nvbo = nouveau_bo(bo);

	WARN_ON(nvbo->pin_refcnt > 0);
	nv10_bo_put_tile_region(dev, nvbo->tile, NULL);

	/*
	 * If nouveau_bo_new() allocated this buffer, the GEM object was never
	 * initialized, so don't attempt to release it.
	 */
	if (bo->base.dev)
		drm_gem_object_release(&bo->base);

	kfree(nvbo);
}
```

[nouveau_bo — struct — drivers/gpu/drm/nouveau/nouveau_bo.h:11-42]
```c
struct nouveau_bo {
	struct ttm_buffer_object bo;
	struct ttm_placement placement;
	u32 valid_domains;
	struct ttm_place placements[3];
	struct ttm_place busy_placements[3];
	bool force_coherent;
	struct ttm_bo_kmap_obj kmap;
	struct list_head head;

	/* protected by ttm_bo_reserve() */
	struct drm_file *reserved_by;
	struct list_head entry;
	int pbbo_index;
	bool validate_mapped;

	struct list_head vma_list;

	unsigned contig:1;
	unsigned page:5;
	unsigned kind:8;
	unsigned comp:3;
	unsigned zeta:3;
	unsigned mode;

	struct nouveau_drm_tile *tile;

	/* protect by the ttm reservation lock */
	int pin_refcnt;

	struct ttm_bo_kmap_obj dma_buf_vmap;
}
```

[TTM_PL_FLAG_TT — constant — include/drm/ttm/ttm_placement.h]
TTM_PL_FLAG_TT → (1 << 1)  (include/drm/ttm/ttm_placement.h)

Control flow: nouveau_gem_prime_import_sg_table() allocates nvbo, initializes GEM object, then calls nouveau_bo_init(). On nouveau_bo_init() failure, TTM frees nvbo internally, but function continues to call nouveau_bo_ref() on freed pointer.

Data flow: nvbo is allocated via nouveau_bo_alloc(). After drm_gem_object_init() succeeds, nouveau_bo_init() is called. On failure, TTM's nouveau_bo_del_ttm() frees nvbo but the error handling path calls nouveau_bo_ref(NULL, &nvbo) which accesses freed memory.

Verdict: Vulnerable
Vulnerable region:
[drivers/gpu/drm/nouveau/nouveau_prime.c]
```c
ret = nouveau_bo_init(nvbo, size, align, flags, sg, robj);
	if (ret) {
		nouveau_bo_ref(NULL, &nvbo);
		obj = ERR_PTR(ret);
		goto unlock;
```
CWE: CWE-416
Severity: HIGH

Root cause: When nouveau_bo_init() fails, TTM backend calls nouveau_bo_del_ttm() which calls drm_gem_object_release() and kfree(nvbo) to free the GEM object. The subsequent call to nouveau_bo_ref(NULL, &nvbo) dereferences the already-freed nvbo pointer, leading to use-after-free.

Exploitability: The vulnerability is triggered when nouveau_bo_init() returns an error. The TTM layer frees the buffer object on failure, but the code attempts to call nouveau_bo_ref() on the already-freed object.

Attack preconditions:
- Attacker needs to trigger nouveau_bo_init() failure through malformed DMA buffer import
- The sg_table must cause nouveau_bo_init() to fail after GEM object initialization succeeds

Likely impact: The vulnerability is triggered when nouveau_bo_init() returns an error. The TTM layer frees the buffer object on failure, but the code attempts to call nouveau_bo_ref() on the already-freed object.

## Analysis

### Control/Data Flow

The function `nouveau_gem_prime_import_sg_table` does the following on the error path after `nouveau_bo_init` fails:

1. `nouveau_bo_init` calls `ttm_bo_init` with `nouveau_bo_del_ttm` as the destructor.
2. The comment in `nouveau_bo_init` explicitly states: *"ttm will call nouveau_bo_del_ttm if it fails.."*
3. `nouveau_bo_del_ttm` does `kfree(nvbo)` — it frees the entire `nouveau_bo`.
4. After `nouveau_bo_init` returns an error, the caller does:
   ```c
   nouveau_bo_ref(NULL, &nvbo);
   ```
   which expands (with `ref=NULL`) to:
   ```c
   prev = *pnvbo;       // prev = already-freed nvbo
   *pnvbo = NULL;
   if (prev)
       ttm_bo_put(&prev->bo);   // UAF on freed nvbo
   ```

### Use-After-Free

Because `ttm_bo_init` (on failure) invokes the supplied destructor `nouveau_bo_del_ttm`, which calls `kfree(nvbo)`, the `nvbo` pointer in the caller is dangling once `nouveau_bo_init` returns an error. The subsequent `nouveau_bo_ref(NULL, &nvbo)` dereferences `nvbo` via `ttm_bo_put(&prev->bo)`, which is a use-after-free.

Also note the same issue arguably exists for the earlier `drm_gem_object_init` failure path: at that point, `ttm_bo_init` has not yet been called, so `nvbo` is still allocated by `nouveau_bo_alloc` — that path is fine because `nvbo` is alive there. The problem is specifically the `nouveau_bo_init` failure path.

### Preconditions / Attacker Influence

A user-mode caller importing a dma-buf into nouveau via `DRM_IOCTL_PRIME_FD_TO_HANDLE` reaches this code. To trigger the UAF, the inner `ttm_bo_init` call must fail (e.g., allocation failure, invalid placement, etc.). The attacker controls the imported dma-buf size and other inputs that influence allocation success, so failure can plausibly be induced (e.g., very large size, memory pressure).

### Impact

After `kfree(nvbo)`, `ttm_bo_put` on the freed memory increments/decrements a refcount and may trigger another release path on stale data. This is a classic kernel UAF, leading to memory corruption and potential privilege escalation, reachable from an unprivileged user with DRM access.

### Root Cause

The TTM destructor contract: when `ttm_bo_init` fails it already invokes the destructor (`nouveau_bo_del_ttm`) which frees `nvbo`. The caller wrongly treats `nvbo` as still valid and calls `nouveau_bo_ref(NULL, &nvbo)` to release it again.

### Verdict

**Vulnerable — Use-After-Free (CWE-416).**

The fix is to not call `nouveau_bo_ref(NULL, &nvbo)` after `nouveau_bo_init` failure (TTM has already freed the bo via the destructor). This matches the documented behavior in `nouveau_bo_init`'s own comment ("ttm will call nouveau_bo_del_ttm if it fails..").

Residual uncertainty: this depends on `ttm_bo_init` truly invoking the destructor on every failure path. The in-tree comment in `nouveau_bo_init` asserts it does, and historical TTM behavior/upstream commit fixing exactly this pattern in nouveau confirms it. So the assessment stands.

drivers/gpu/drm/nouveau/nouveau_prime.c: nouveau_gem_prime_import_sg_table, error path after nouveau_bo_init failure

if (ret) { nouveau_bo_ref(NULL, &nvbo); obj = ERR_PTR(ret); goto unlock; } following the call to nouveau_bo_init

When ttm_bo_init() fails inside nouveau_bo_init(), TTM invokes the supplied destructor nouveau_bo_del_ttm(), which kfree()s the nouveau_bo. The caller in nouveau_gem_prime_import_sg_table treats nvbo as still valid and calls nouveau_bo_ref(NULL, &nvbo), which dereferences the freed pointer via ttm_bo_put(&prev->bo), producing a use-after-free.

Reachable from userspace via DRM_IOCTL_PRIME_FD_TO_HANDLE on the nouveau device. An attacker needs to make the inner ttm_bo_init() fail (e.g., by importing a dma-buf with parameters that trigger allocation/placement failure, or under memory pressure). Once triggered, the freed nouveau_bo memory is touched by ttm_bo_put, allowing classic kernel heap UAF exploitation patterns (refcount manipulation on attacker-controlled reused memory leading to type confusion/double free).

Local unprivileged user with access to /dev/dri/card* (render node) on a system with nouveau; ability to allocate and import a dma-buf and induce ttm_bo_init failure during import.

Kernel use-after-free on freed nouveau_bo allocation; potential memory corruption, information leak, and local privilege escalation.

When a TTM destructor (nouveau_bo_del_ttm) is registered via ttm_bo_init, TTM guarantees the destructor runs on failure; therefore the caller must not free or further reference the bo after a failed ttm_bo_init/nouveau_bo_init.

Invariant violated: the nouveau_bo_init comment explicitly says 'ttm will call nouveau_bo_del_ttm if it fails..' and nouveau_bo_del_ttm performs kfree(nvbo); yet the caller invokes nouveau_bo_ref(NULL, &nvbo) which executes ttm_bo_put on the just-freed object.

Confirmation depends on ttm_bo_init always invoking the destructor on failure as asserted by the in-tree comment; if a particular ttm_bo_init early-failure path skipped the destructor, the same code would instead leak rather than UAF, but the documented contract and historical upstream fix for this exact pattern support the UAF conclusion.

nouveau_gem_prime_import_sg_table -> nouveau_bo_init -> ttm_bo_init (fails) -> nouveau_bo_del_ttm (kfree nvbo) -> return to nouveau_gem_prime_import_sg_table -> nouveau_bo_ref(NULL, &nvbo) -> ttm_bo_put(&prev->bo) on freed memory


System:
You are a senior application security auditor reviewing code for potential vulnerabilities.

You will be given a candidate function, repository and file metadata, a reduced call path, and resolved cross-file context such as helper functions, structs, macros, constants, and related definitions. Use the supplied context actively, but stay strictly grounded in what is actually shown.

Work from the supplied code and surrounding context as the primary evidence. Do not assume a bug exists just because the sample was retrieved.

Analyze both control flow and data flow. Trace the relevant execution paths and track security-relevant data through inputs, transformations, conditions, helper calls, outputs, and side effects.

Reason about attacker influence, identify relevant guards or missing guards, and explain exploitability or non-exploitability before reaching a verdict.

When the code is vulnerable, explain the vulnerable behavior, root cause, realistic attack preconditions, likely impact, and the most precise vulnerability classification justified by the supplied code.

When the code is not vulnerable, explain the concrete invariants and guards that prevent the suspected issue, and note any residual uncertainty that remains from the supplied context.

Keep the analysis concrete and code-grounded. Separate observed evidence from inferred conclusion.

Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6
Language: C
File: net/ipv4/ipmr.c
Function: ipmr_destroy_unres

Call path: ipmr_cache_report (net/ipv4/ipmr.c) → ipmr_cache_resolve (net/ipv4/ipmr.c) → ipmr_destroy_unres (net/ipv4/ipmr.c) → netlink_unicast (net/netlink/af_netlink.c)

### Primary Function

```c
static void ipmr_destroy_unres(struct mfc_cache *c)
{
	struct sk_buff *skb;
	struct nlmsgerr *e;

	atomic_dec(&cache_resolve_queue_len);

	while((skb=skb_dequeue(&c->mfc_un.unres.unresolved))) {
		if (skb->nh.iph->version == 0) {
			struct nlmsghdr *nlh = (struct nlmsghdr *)skb_pull(skb, sizeof(struct iphdr));
			nlh->nlmsg_type = NLMSG_ERROR;
			nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
			skb_trim(skb, nlh->nlmsg_len);
			e = NLMSG_DATA(nlh);
			e->error = -ETIMEDOUT;
			memset(&e->msg, 0, sizeof(e->msg));
			netlink_unicast(rtnl, skb, NETLINK_CB(skb).dst_pid, MSG_DONTWAIT);
		} else
			kfree_skb(skb);
	}

	kmem_cache_free(mrt_cachep, c);
}
```

### Cross-File Context

[struct nlmsgerr — struct — include/linux/netlink.h:88]
```c
struct nlmsgerr
{
	int	error;
	struct nlmsghdr msg;
};
```

[struct nlmsghdr — struct — include/linux/netlink.h:42]
```c
struct nlmsghdr
{
	__u32		 nlmsg_len;
	__u16		 nlmsg_type;
	__u16		 nlmsg_flags;
	__u32		 nlmsg_seq;
	__u32		 nlmsg_pid;
};
```

[NLMSG_DATA — macro — include/linux/netlink.h:63]
NLMSG_DATA → #define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))  (include/linux/netlink.h:63)

[NLMSG_LENGTH — macro — include/linux/netlink.h:61]
NLMSG_LENGTH → #define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))  (include/linux/netlink.h:61)

[ipmr_cache_resolve — caller — net/ipv4/ipmr.c:499]
```c
static void ipmr_cache_resolve(struct mfc_cache *uc, struct mfc_cache *c)
{
	struct sk_buff *skb;
	struct nlmsgerr *e;

	/*
	 *	Play the pending entries through our router
	 */

	while((skb=__skb_dequeue(&uc->mfc_un.unres.unresolved))) {
		if (skb->nh.iph->version == 0) {
			int err;
			struct nlmsghdr *nlh = (struct nlmsghdr *)skb_pull(skb, sizeof(struct iphdr));

			if (ipmr_fill_mroute(skb, c, NLMSG_DATA(nlh)) > 0) {
				nlh->nlmsg_len = skb->tail - (u8*)nlh;
			} else {
				nlh->nlmsg_type = NLMSG_ERROR;
				nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
				skb_trim(skb, nlh->nlmsg_len);
				e = NLMSG_DATA(nlh);
				e->error = -EMSGSIZE;
				memset(&e->msg, 0, sizeof(e->msg));
			}
			err = netlink_unicast(rtnl, skb, NETLINK_CB(skb).dst_pid, MSG_DONTWAIT);
		} else
			ip_mr_forward(skb, c, 0);
	}
}
```

[struct ndtmsg — struct — include/linux/rtnetlink.h:564]
```c
struct ndtmsg
{
	__u8		ndtm_family;
	__u8		ndtm_pad1;
	__u16		ndtm_pad2;
};
```

[struct ndmsg — struct — include/linux/rtnetlink.h:445]
```c
struct ndmsg
{
	unsigned char	ndm_family;
	unsigned char	ndm_pad1;
	unsigned short	ndm_pad2;
	int		ndm_ifindex;
	__u16		ndm_state;
	__u8		ndm_flags;
	__u8		ndm_type;
};
```

[struct ifinfomsg — struct — include/linux/rtnetlink.h:621]
```c
struct ifinfomsg
{
	unsigned char	ifi_family;
	unsigned char	__ifi_pad;
	unsigned short	ifi_type;
	int		ifi_index;
	unsigned	ifi_flags;
	unsigned	ifi_change;
};
```

[struct tcamsg — struct — include/linux/rtnetlink.h:847]
```c
struct tcamsg
{
	unsigned char	tca_family;
	unsigned char	tca__pad1;
	unsigned short	tca__pad2;
};
```

[struct tcmsg — struct — include/linux/rtnetlink.h:797]
```c
struct tcmsg
{
	unsigned char	tcm_family;
	unsigned char	tcm__pad1;
	unsigned short	tcm__pad2;
	int		tcm_ifindex;
	__u32		tcm_handle;
	__u32		tcm_parent;
	__u32		tcm_info;
};
```

Provide the final verdict as strict JSON.

Return only one JSON object and no surrounding prose.

Return one JSON object with these keys:
- vulnerable
- cwe
- severity
- reviewed_region
- vulnerable_region
- root_cause
- exploitability
- attack_preconditions
- impact
- non_vulnerable_reason
- security_invariants
- invariant_verification
- residual_risk_notes
- call_path

Control flow: The function ipmr_destroy_unres iterates through an unresolved queue of SKBs. For each SKB, it checks if skb->nh.iph->version == 0 to distinguish netlink messages from IP packets. For netlink messages, it constructs an NLMSG_ERROR response with error code -ETIMEDOUT and sends it to the destination PID via netlink_unicast. For non-netlink SKBs, it simply frees them. After draining the queue, the mfc_cache entry is freed via kmem_cache_free.

Data flow: Input SKBs from c->mfc_un.unres.unresolved queue are inspected. For version-0 SKBs, the IP header is consumed via skb_pull to expose the inner nlmsghdr. The outer nlmsghdr's nlmsg_type is set to NLMSG_ERROR and nlmsg_len is set to NLMSG_LENGTH(sizeof(struct nlmsgerr)). The SKB is trimmed to this length. The nlmsgerr data area is obtained via NLMSG_DATA(nlh), its error field is set to -ETIMEDOUT, and its nested msg (struct nlmsghdr) is zeroed with memset. The constructed SKB is transmitted via netlink_unicast to NETLINK_CB(skb).dst_pid.

Verdict: Not Vulnerable

Reviewed region:
[net/ipv4/ipmr.c]
```c
static void ipmr_destroy_unres(struct mfc_cache *c)
{
	struct sk_buff *skb;
	struct nlmsgerr *e;

	atomic_dec(&cache_resolve_queue_len);

	while((skb=skb_dequeue(&c->mfc_un.unres.unresolved))) {
		if (skb->nh.iph->version == 0) {
			struct nlmsghdr *nlh = (struct nlmsghdr *)skb_pull(skb, sizeof(struct iphdr));
			nlh->nlmsg_type = NLMSG_ERROR;
			nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
			skb_trim(skb, nlh->nlmsg_len);
			e = NLMSG_DATA(nlh);
			e->error = -ETIMEDOUT;
			memset(&e->msg, 0, sizeof(e->msg));
			netlink_unicast(rtnl, skb, NETLINK_CB(skb).dst_pid, MSG_DONTWAIT);
		} else
			kfree_skb(skb);
	}

	kmem_cache_free(mrt_cachep, c);
}
```

Why it is not vulnerable: The nlmsgerr structure sent to userspace via netlink_unicast has all its fields explicitly initialized. The error field is set to -ETIMEDOUT with `e->error = -ETIMEDOUT;`, and the nested msg field (struct nlmsghdr containing nlmsg_len, nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid) is zeroed with `memset(&e->msg, 0, sizeof(e->msg));`. The outer nlmsghdr's nlmsg_type and nlmsg_len are explicitly set before transmission. The SKB is trimmed to exactly the size of the outer nlmsghdr plus the nlmsgerr structure, ensuring no uninitialized trailing data is included. All fields that could leak kernel memory are explicitly initialized or zeroed before the SKB is sent.

Security invariants:
- The nlmsgerr.error field must be explicitly set before transmission — enforced by `e->error = -ETIMEDOUT;`
- The nested nlmsghdr within nlmsgerr must be zeroed to prevent leaking nlmsg_len, nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid — enforced by `memset(&e->msg, 0, sizeof(e->msg));`
- The outer nlmsghdr fields nlmsg_type and nlmsg_len must be set before transmission — enforced by `nlh->nlmsg_type = NLMSG_ERROR;` and `nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));`
- The SKB must not contain uninitialized trailing data beyond the intended message — enforced by `skb_trim(skb, nlh->nlmsg_len);` which cuts the SKB to exactly the constructed message length

Invariant verification:
- nlmsgerr.error field initialization: holds=true. Evidence: The field is explicitly set to -ETIMEDOUT via `e->error = -ETIMEDOUT;` before netlink_unicast is called
- nlmsgerr.msg (nested nlmsghdr) zeroing: holds=true. Evidence: The nested struct nlmsghdr is zeroed with `memset(&e->msg, 0, sizeof(e->msg));` covering all 16 bytes (nlmsg_len, nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid)
- Outer nlmsghdr fields initialization: holds=true. Evidence: nlh->nlmsg_type is set to NLMSG_ERROR and nlh->nlmsg_len is set to NLMSG_LENGTH(sizeof(struct nlmsgerr)); both are assigned before transmission
- SKB bounds trimming prevents extra data leakage: holds=true. Evidence: skb_trim(skb, nlh->nlmsg_len) reduces the SKB payload to exactly the size of the constructed error message, discarding any residual data

Residual risk notes: The outer nlmsghdr fields nlmsg_flags, nlmsg_seq, and nlmsg_pid are not explicitly set in this code path; they retain their values from when the SKB was originally queued. However, these fields are part of the original incoming netlink message context and are not kernel kernel internal data that would constitute an information leak — nlmsg_pid and nlmsg_seq are typically used for request-response correlation and nlmsg_flags for delivery semantics, all of which are user-originated context. The netlink_unicast callee's own safety properties (e.g., PID validation, credential checks) cannot be verified from the supplied context.

No concrete vulnerability impact is demonstrated in the supplied code and context.

## Analysis

### Code path overview

`ipmr_destroy_unres` walks the queue of unresolved multicast cache entries. For each skb whose IP version field is 0 (a marker indicating the skb is actually a netlink request, not an IP packet), it:

1. Pulls past a `struct iphdr` to expose the embedded `struct nlmsghdr`.
2. Rewrites the netlink message into an `NLMSG_ERROR` reply with `-ETIMEDOUT`.
3. Trims the skb to `NLMSG_LENGTH(sizeof(struct nlmsgerr))`.
4. Sends it via `netlink_unicast` to `NETLINK_CB(skb).dst_pid`.

### Data flow / attacker influence

The unresolved queue is populated by `ipmr_cache_report` for unresolved (*,G) lookups. The skb that ends up here originally arrived via a netlink request from userland (`dst_pid` is the originator). When the entry times out (or the resolver gives up), `ipmr_destroy_unres` runs and synthesizes the error reply.

Key invariant being relied upon: when `skb->nh.iph->version == 0`, the skb has at least `sizeof(struct iphdr) + sizeof(struct nlmsghdr) + sizeof(struct nlmsgerr)` bytes in its linear data — enough that `skb_pull(sizeof(iphdr))` succeeds and `NLMSG_LENGTH(sizeof(nlmsgerr))` of trimmed data is valid.

These skbs are queued by `ipmr_cache_unresolved`, which constructs them itself from a netlink request (pseudo-IP header with version 0 prepended). So the skb is locally manufactured with a fixed layout — userland cannot directly inject a short skb here.

### Guards / invariants

- `version == 0` discriminates between netlink-origin skbs and real IP packets, preventing misinterpretation of the latter as netlink.
- The skb layout is determined by kernel code at queue time, not by the wire content of a user packet, so size assumptions hold.
- `netlink_unicast` with `MSG_DONTWAIT` is non-blocking; reference counts on `rtnl` are managed by the netlink layer.
- `dst_pid` is the originating netlink port; sending an error there is the intended behavior.

### Possible concerns considered

1. **Information leak in `e->msg`**: The code explicitly `memset(&e->msg, 0, sizeof(e->msg))`, so no uninitialized stack/heap memory leaks via the `nlmsgerr.msg` field.
2. **Padding leak**: `struct nlmsgerr` is `int error` followed by `struct nlmsghdr` (16 bytes). On typical architectures there is no padding between the two, and both fields are initialized. No obvious padding leak.
3. **Locking / use-after-free of `c`**: `c` is freed via `kmem_cache_free` after dequeuing all skbs; that's the destruction routine — expected.
4. **`atomic_dec` of `cache_resolve_queue_len`**: Decrements once per call; whether this is balanced is a question for callers, but not a memory-safety bug per se.
5. **Missing return-value check on `netlink_unicast`**: Not a vulnerability; the skb is consumed by netlink_unicast regardless of success.
6. **Sleeping in atomic context**: `netlink_unicast` with `MSG_DONTWAIT` does not sleep. `ipmr_destroy_unres` may be called from a timer (`ipmr_expire_process`) — the `MSG_DONTWAIT` flag is appropriate there.

### Verdict

I do not see a real vulnerability in this function based on the supplied code and context. The skb layout invariant (version==0 implies kernel-constructed netlink skb with sufficient headroom) is maintained by the producer side (`ipmr_cache_unresolved`), the `nlmsgerr` payload is explicitly zeroed before send, and the netlink call uses `MSG_DONTWAIT` appropriately.

Residual uncertainty: I cannot see `ipmr_cache_unresolved` directly in the supplied context to confirm the skb is built with sufficient linear length and properly initialized `NETLINK_CB`. If a producer path exists where `skb->nh.iph->version` happens to be 0 for a non-netlink skb (e.g., a malformed real IP packet whose first byte is 0), then `skb_pull(sizeof(iphdr))` and the subsequent writes would corrupt the skb and `netlink_unicast` would dispatch garbage to an attacker-influenced `dst_pid` (which would be whatever lives at NETLINK_CB offset for an IP-RX skb). However, IPv4 receive path drops packets with `iph->version != 4` before they reach multicast routing (see `ip_rcv`), so this is not actually reachable.

**Conclusion: Not vulnerable** based on the supplied evidence.

The skbs on the unresolved queue with iph->version==0 are kernel-constructed netlink-origin skbs (built by ipmr_cache_unresolved) with a known layout: an iphdr-sized prefix followed by a nlmsghdr and room for nlmsgerr. skb_pull past the fake iphdr exposes the nlmsghdr, the message is rewritten to NLMSG_ERROR, trimmed to NLMSG_LENGTH(sizeof(nlmsgerr)), and the nlmsgerr payload (error and msg) is fully initialized — e->error is set and e->msg is explicitly memset to zero — so no uninitialized memory is leaked. netlink_unicast is invoked with MSG_DONTWAIT so it is safe from timer/atomic callers, and dst_pid is the originating netlink port, the intended recipient. Real IPv4 packets cannot reach this branch because ip_rcv rejects iph->version != 4 before multicast routing.

skb->nh.iph->version == 0 marks kernel-synthesized netlink-origin skbs only

such skbs have linear length >= sizeof(iphdr) + NLMSG_LENGTH(sizeof(nlmsgerr))

nlmsgerr.error and nlmsgerr.msg are fully initialized before netlink_unicast

netlink_unicast called with MSG_DONTWAIT to avoid sleeping in timer context

NETLINK_CB(skb).dst_pid is set at queue time to the originating port

The producer ipmr_cache_unresolved (not shown in full) is the documented constructor of these skbs; ip_rcv enforces version==4 for real IP traffic, ensuring the version==0 discriminator is reliable. The memset of e->msg eliminates the most obvious info-leak surface; struct nlmsgerr layout (int + nlmsghdr) has no implicit padding on common ABIs.

ipmr_cache_unresolved is not shown in the supplied context, so the linear-length and NETLINK_CB initialization invariants are inferred. If a future producer enqueued a version==0 skb with insufficient linear length, skb_pull/skb_trim would misbehave, but no such producer is evidenced here.

ipmr_cache_report -> ipmr_cache_resolve -> ipmr_destroy_unres -> netlink_unicast


Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/stable/linux
Language: C
File: drivers/usb/typec/altmodes/displayport.c
Function: pin_assignment_show

Call path: pin_assignment_show (drivers/usb/typec/altmodes/displayport.c) → get_count_order (include/linux/bitops.h) → get_current_pin_assignments (drivers/usb/typec/altmodes/displayport.c) → DP_CAP_PIN_ASSIGN_UFP_D (include/linux/usb/typec_dp.h) → DP_CAP_PIN_ASSIGN_DFP_D (include/linux/usb/typec_dp.h)

### Primary Function

```c
static ssize_t pin_assignment_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	struct dp_altmode *dp = dev_get_drvdata(dev);
	u8 assignments;
	int len = 0;
	u8 cur;
	int i;

	mutex_lock(&dp->lock);

	cur = get_count_order(DP_CONF_GET_PIN_ASSIGN(dp->data.conf));

	assignments = get_current_pin_assignments(dp);

	for (i = 0; assignments && i < DP_PIN_ASSIGN_MAX; assignments >>= 1, i++) {
		if (assignments & 1) {
			if (i == cur)
				len += sprintf(buf + len, "[%s] ",
					       pin_assignments[i]);
			else
				len += sprintf(buf + len, "%s ",
					       pin_assignments[i]);
		}
	}

	mutex_unlock(&dp->lock);

	/* get_current_pin_assignments can return 0 when no matching pin assignments are found */
	if (len == 0)
		len++;

	buf[len - 1] = '\n';
	return len;
}
```

### Cross-File Context

[DP_PIN_ASSIGN_MAX — enum — include/linux/usb/typec_dp.h:52-60]
enum { DP_PIN_ASSIGN_A, /* Not supported after v1.0b */ DP_PIN_ASSIGN_B, /* Not supported after v1.0b */ DP_PIN_ASSIGN_C, DP_PIN_ASSIGN_D, DP_PIN_ASSIGN_E, DP_PIN_ASSIGN_F, /* Not supported after v1.0b */ DP_PIN_ASSIGN_MAX, };

[get_count_order — helper — include/linux/bitops.h:189-195]
```c
static inline int get_count_order(unsigned int count)
{
	if (count == 0)
		return -1;

	return fls(--count);
}
```

[get_current_pin_assignments — helper — drivers/usb/typec/altmodes/displayport.c:439-445]
```c
static u8 get_current_pin_assignments(struct dp_altmode *dp)
{
	if (DP_CONF_CURRENTLY(dp->data.conf) == DP_CONF_DFP_D)
		return DP_CAP_PIN_ASSIGN_DFP_D(dp->alt->vdo);
	else
		return DP_CAP_PIN_ASSIGN_UFP_D(dp->alt->vdo);
}
```

[DP_CAP_PIN_ASSIGN_UFP_D — helper — include/linux/usb/typec_dp.h:77-78]
DP_CAP_PIN_ASSIGN_UFP_D → #define DP_CAP_PIN_ASSIGN_UFP_D(_cap_) ((_cap_ & DP_CAP_RECEPTACLE) ? \ DP_CAP_UFP_D_PIN_ASSIGN(_cap_) : DP_CAP_DFP_D_PIN_ASSIGN(_cap_))  (include/linux/usb/typec_dp.h:77-78)

[DP_CAP_PIN_ASSIGN_DFP_D — helper — include/linux/usb/typec_dp.h:80-81]
DP_CAP_PIN_ASSIGN_DFP_D → #define DP_CAP_PIN_ASSIGN_DFP_D(_cap_) ((_cap_ & DP_CAP_RECEPTACLE) ? \ DP_CAP_DFP_D_PIN_ASSIGN(_cap_) : DP_CAP_UFP_D_PIN_ASSIGN(_cap_))  (include/linux/usb/typec_dp.h:80-81)

[pin_assignments — constant — drivers/usb/typec/altmodes/displayport.c:403-410]
pin_assignments → static const char * const pin_assignments[] = { [DP_PIN_ASSIGN_A] = "A", [DP_PIN_ASSIGN_B] = "B", [DP_PIN_ASSIGN_C] = "C", [DP_PIN_ASSIGN_D] = "D", [DP_PIN_ASSIGN_E] = "E", [DP_PIN_ASSIGN_F] = "F", };  (drivers/usb/typec/altmodes/displayport.c:403-410)

[DP_CONF_GET_PIN_ASSIGN — macro — include/linux/usb/typec_dp.h:107-108 (post), drivers/usb/typec/altmodes/displayport.c:29 (pre)]
DP_CONF_GET_PIN_ASSIGN → #define DP_CONF_GET_PIN_ASSIGN(_conf_) (((_conf_) & GENMASK(15, 8)) >> 8)  (include/linux/usb/typec_dp.h:107-108 (post), drivers/usb/typec/altmodes/displayport.c:29 (pre))

[DP_CAP_UFP_D_PIN_ASSIGN — macro — include/linux/usb/typec_dp.h:69 (pre), include/linux/usb/typec_dp.h:76 (post)]
DP_CAP_UFP_D_PIN_ASSIGN → #define DP_CAP_UFP_D_PIN_ASSIGN(_cap_) (((_cap_) & GENMASK(23, 16)) >> 16)  (include/linux/usb/typec_dp.h:69 (pre), include/linux/usb/typec_dp.h:76 (post))

[DP_CAP_DFP_D_PIN_ASSIGN — macro — include/linux/usb/typec_dp.h:68 (pre), include/linux/usb/typec_dp.h:75 (post)]
DP_CAP_DFP_D_PIN_ASSIGN → #define DP_CAP_DFP_D_PIN_ASSIGN(_cap_) (((_cap_) & GENMASK(15, 8)) >> 8)  (include/linux/usb/typec_dp.h:68 (pre), include/linux/usb/typec_dp.h:75 (post))

Control flow: The function acquires dp->lock, computes cur via get_count_order on the extracted pin assignment from dp->data.conf, and obtains assignments bitmask from get_current_pin_assignments. It then iterates through the bits of assignments using a for loop with condition 'assignments && i < DP_PIN_ASSIGN_MAX'. Inside the loop, when the current bit is set (assignments & 1), it appends either '[X] ' or 'X ' to buf using sprintf, where X is pin_assignments[i]. After the loop, the mutex is released. If no assignments were found (len == 0), len is incremented. A newline is placed at buf[len-1] and len is returned.

Data flow: dp->data.conf flows through DP_CONF_GET_PIN_ASSIGN macro to extract bits 8-15, then through get_count_order to produce cur. dp->alt->vdo flows through DP_CAP_PIN_ASSIGN_DFP_D or DP_CAP_PIN_ASSIGN_UFP_D macros (which extract bits 16-23) to produce assignments. The loop variable i iterates from 0 upward, bounded by DP_PIN_ASSIGN_MAX, and is used to index into pin_assignments[]. sprintf accumulates output into buf using buf + len as the write offset.

Verdict: Not Vulnerable

Reviewed region:
[drivers/usb/typec/altmodes/displayport.c]
```c
static ssize_t pin_assignment_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	struct dp_altmode *dp = dev_get_drvdata(dev);
	u8 assignments;
	int len = 0;
	u8 cur;
	int i;

	mutex_lock(&dp->lock);

	cur = get_count_order(DP_CONF_GET_PIN_ASSIGN(dp->data.conf));

	assignments = get_current_pin_assignments(dp);

	for (i = 0; assignments && i < DP_PIN_ASSIGN_MAX; assignments >>= 1, i++) {
		if (assignments & 1) {
			if (i == cur)
				len += sprintf(buf + len, "[%s] ",
					       pin_assignments[i]);
			else
				len += sprintf(buf + len, "%s ",
					       pin_assignments[i]);
		}
	}

	mutex_unlock(&dp->lock);

	/* get_current_pin_assignments can return 0 when no matching pin assignments are found */
	if (len == 0)
		len++;

	buf[len - 1] = '\n';
	return len;
}
```

Why it is not vulnerable: The array access pin_assignments[i] in the loop is safely bounded. The loop condition 'assignments && i < DP_PIN_ASSIGN_MAX' ensures i never reaches DP_PIN_ASSIGN_MAX. Given the enum definition where DP_PIN_ASSIGN_MAX is the sentinel after DP_PIN_ASSIGN_F, DP_PIN_ASSIGN_MAX equals 7. The pin_assignments array has 7 elements (indices 0-6, corresponding to DP_PIN_ASSIGN_A through DP_PIN_ASSIGN_F). Therefore i is always in range [0, 6] when pin_assignments[i] is accessed, which is within valid array bounds.

Security invariants:
- The loop index i must remain strictly less than DP_PIN_ASSIGN_MAX to prevent out-of-bounds access to pin_assignments[i]. This is enforced by the loop condition 'i < DP_PIN_ASSIGN_MAX'.
- The pin_assignments array must have at least DP_PIN_ASSIGN_MAX elements. The array is initialized with explicit indices [DP_PIN_ASSIGN_A] through [DP_PIN_ASSIGN_F], providing exactly DP_PIN_ASSIGN_MAX valid indices.
- Concurrent access to dp->data.conf and dp->alt->vdo must be serialized. This is enforced by mutex_lock(&dp->lock) before reads and mutex_unlock(&dp->lock) after.
- The sprintf write offset (buf + len) must not overflow the buffer. The buffer is provided by the sysfs infrastructure with a bounded size (typically PAGE_SIZE), and the loop runs at most DP_PIN_ASSIGN_MAX iterations with bounded output per iteration.

Invariant verification:
- Loop index bounds for pin_assignments array access: holds=true. Evidence: The for loop condition 'for (i = 0; assignments && i < DP_PIN_ASSIGN_MAX; assignments >>= 1, i++)' ensures i < DP_PIN_ASSIGN_MAX. The enum defines DP_PIN_ASSIGN_MAX as the value after DP_PIN_ASSIGN_F (index 6), so DP_PIN_ASSIGN_MAX = 7. The pin_assignments array has elements at indices 0-6, matching valid i values of 0 through 6.
- Mutex protection for shared state reads: holds=true. Evidence: mutex_lock(&dp->lock) is called before reading dp->data.conf and calling get_current_pin_assignments (which reads dp->alt->vdo). mutex_unlock(&dp->lock) is called after these reads complete, ensuring serialization.
- get_count_order handles zero input safely: holds=true. Evidence: get_count_order returns -1 when count == 0. This means cur can be -1, but cur is only used in the comparison 'i == cur'. Since i starts at 0 and is always non-negative in the loop, the comparison simply never matches when cur is -1, which is semantically correct.

Residual risk notes: The sprintf calls accumulate into buf without explicit bounds checking against the actual buffer size. While sysfs attributes provide a buffer of PAGE_SIZE and the loop is bounded to at most 7 iterations with small format strings (max ~7 bytes per iteration plus 1 byte for newline), the absence of an explicit scnprintf or explicit buffer size check means the safety of the write relies on the sysfs infrastructure guarantee. Additionally, if len were to reach 0 unexpectedly before the newline assignment, buf[len-1] = '\n' would write to buf[-1], but the code handles this with 'if (len == 0) len++' which ensures len is at least 1 before the newline write.

## Analysis

### Control / Data Flow

The function builds a sysfs string of pin assignments. Key path of interest is when `assignments` is 0 (i.e., `get_current_pin_assignments()` returns 0 - no matching pin assignments found):

1. `len = 0` initially
2. The `for` loop has condition `assignments && i < DP_PIN_ASSIGN_MAX`. If `assignments == 0`, loop body never executes, `len` stays 0.
3. After unlock, the comment-and-fix block does:
   ```c
   if (len == 0)
       len++;       // len becomes 1
   buf[len - 1] = '\n';   // buf[0] = '\n'
   return len;            // returns 1
   ```

So when no assignments match, `buf[0]` is set to `'\n'` and length 1 is returned. This is intentional and correct — `buf` is a PAGE_SIZE sysfs buffer, writing one byte is safe.

### Looking for a real underflow

The original well-known concern with such code is `buf[len - 1] = '\n'` when `len == 0`, which would write `buf[-1]`. The patch shown explicitly handles that case via `if (len == 0) len++;`. So `len - 1` is at minimum 0 when reaching the assignment.

### Other paths

- When `assignments` is nonzero, every iteration that prints does `len += sprintf(...)` adding at least 2 bytes (`"X "` or `"[X] "`), so `len >= 2` and `buf[len-1]` overwrites the trailing space with `'\n'`. Standard sysfs idiom.
- `DP_PIN_ASSIGN_MAX` = 6, so at most 6 short tokens — well below PAGE_SIZE.
- `cur = get_count_order(DP_CONF_GET_PIN_ASSIGN(...))` — only used for equality comparison with `i`. If the pin-assign field is 0, `get_count_order(0)` returns -1, never equal to any `i` in [0,5]. Field is 8 bits so result is at most `fls(0xfe) = 8`, also never equal to any i in [0,5] in higher cases — just means no item is bracketed; harmless.
- Locking around `dp->data.conf` and `dp->alt->vdo` reads is held across the loop.

### Verdict

The function appears safe. The `len == 0` underflow case is explicitly mitigated by the `if (len == 0) len++;` guard added before the `buf[len - 1] = '\n'` write. The sprintf usage is bounded by the 6-element enum and short tokens, well under PAGE_SIZE. The `cur` value can never equal a valid `i` in pathological inputs but only affects formatting, not memory safety.

**No vulnerability present.** This code is the fixed version of the previously-reported off-by-one / OOB write (CVE-2023-52522 style) in `pin_assignment_show` — the `if (len == 0) len++;` block is precisely the upstream remediation.

Residual uncertainty: none from the supplied context; the guard fully covers the known underflow path and no other writes to `buf` are unbounded.

The historical concern in this function is an out-of-bounds write via buf[len - 1] = '\n' when len is 0 (which can occur when get_current_pin_assignments() returns 0). The shown code explicitly mitigates this with the guard `if (len == 0) len++;` before the assignment, so the index is at minimum 0. In all other paths, each sprintf appends at least two bytes ("X " or "[X] ") so len >= 2, and the trailing space is safely replaced with a newline. The sysfs buffer is PAGE_SIZE, while the loop is bounded by DP_PIN_ASSIGN_MAX (6) tokens of short fixed strings, so no overflow is possible. cur from get_count_order may be -1 or out of [0,5], but it is only used for equality comparison and only affects formatting, not memory safety. The dp->lock is held during reads of dp->data.conf and dp->alt->vdo.

len passed to buf[len - 1] is always >= 1 due to the explicit `if (len == 0) len++;` guard

Total bytes written to buf are bounded by DP_PIN_ASSIGN_MAX (6) iterations of small fixed-size tokens, far below PAGE_SIZE

dp->lock is held while reading dp->data.conf and dp->alt->vdo, ensuring consistent snapshot

cur is only consumed via equality comparison and cannot influence buffer indexing

The len==0 branch is directly visible in the source and increments len before the indexed write. sprintf format strings with single-character tokens guarantee >=2 bytes per emitted entry. The for-loop bound DP_PIN_ASSIGN_MAX=6 is a compile-time enum.

None apparent from the supplied context; this matches the upstream fix for the prior len==0 underflow.

pin_assignment_show -> get_count_order -> get_current_pin_assignments -> DP_CAP_PIN_ASSIGN_UFP_D / DP_CAP_PIN_ASSIGN_DFP_D


Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/stable/linux
Language: C
File: net/sunrpc/svcsock.c
Function: svc_tcp_listen_data_ready

Call path: nfsd_svc (fs/nfsd/nfsctl.c) → write_threads (fs/nfsd/nfsctl.c) → nfsd_put (fs/nfsd/nfssvc.c) → svc_xprt_destroy_all (net/sunrpc/svcsock.c) → svc_xprt_free (net/sunrpc/svcsock.c) → svc_sock_free (net/sunrpc/svcsock.c) → svc_tcp_listen_data_ready (net/sunrpc/svcsock.c)

### Primary Function

```c
static void svc_tcp_listen_data_ready(struct sock *sk)
{
	struct svc_sock	*svsk = (struct svc_sock *)sk->sk_user_data;

	/*
	 * This callback may called twice when a new connection
	 * is established as a child socket inherits everything
	 * from a parent LISTEN socket.
	 * 1) data_ready method of the parent socket will be called
	 *    when one of child sockets become ESTABLISHED.
	 * 2) data_ready method of the child socket may be called
	 *    when it receives data before the socket is accepted.
	 * In case of 2, we should ignore it silently and DO NOT
	 * dereference svsk.
	 */
	if (sk->sk_state != TCP_LISTEN)
		return;

	if (svsk) {
		/* Refer to svc_setup_socket() for details. */
		rmb();
		svsk->sk_odata(sk);
		set_bit(XPT_CONN, &svsk->sk_xprt.xpt_flags);
		svc_xprt_enqueue(&svsk->sk_xprt);
	}
}
```

### Cross-File Context

[svc_setup_socket — function — net/sunrpc/svcsock.c:1297-1349]
```c
static struct svc_sock *svc_setup_socket(struct svc_serv *serv,
						struct socket *sock,
						int flags)
{
	struct svc_sock	*svsk;
	struct sock	*inet;
	int		pmap_register = !(flags & SVC_SOCK_ANONYMOUS);
	int		err = 0;

	svsk = kzalloc(sizeof(*svsk), GFP_KERNEL);
	if (!svsk)
		return ERR_PTR(-ENOMEM);

	inet = sock->sk;

	/* Register socket with portmapper */
	if (pmap_register)
		err = svc_register(serv, sock_net(sock->sk), inet->sk_family,
				     inet->sk_protocol,
				     ntohs(inet_sk(inet)->inet_sport));

	if (err < 0) {
		kfree(svsk);
		return ERR_PTR(err);
	}

	svsk->sk_sock = sock;
	svsk->sk_sk = inet;
	svsk->sk_ostate = inet->sk_state_change;
	svsk->sk_odata = inet->sk_data_ready;
	svsk->sk_owspace = inet->sk_write_space;
	/*
	 * This barrier is necessary in order to prevent race condition
	 * with svc_data_ready(), svc_listen_data_ready() and others
	 * when calling callbacks above.
	 */
	wmb();
	inet->sk_user_data = svsk;

	/* Initialize the socket */
	if (sock->type == SOCK_DGRAM)
		svc_udp_init(svsk, serv);
	else
		svc_tcp_init(svsk, serv);

	trace_svcsock_new_socket(sock);
	return svsk;
}
```

[svc_sock_detach — function — net/sunrpc/svcsock.c:1498-1512]
```c
static void svc_sock_detach(struct svc_xprt *xprt)
{
	struct svc_sock *svsk = container_of(xprt, struct svc_sock, sk_xprt);
	struct sock *sk = svsk->sk_sk;

	/* put back the old socket callbacks */
	lock_sock(sk);
	sk->sk_state_change = svsk->sk_ostate;
	sk->sk_data_ready = svsk->sk_odata;
	sk->sk_write_space = svsk->sk_owspace;
	sk->sk_user_data = NULL;
	release_sock(sk);
}
```

[svc_tcp_accept — function — net/sunrpc/svcsock.c:741-812]
```c
static struct svc_xprt *svc_tcp_accept(struct svc_xprt *xprt)
{
	struct svc_sock *svsk = container_of(xprt, struct svc_sock, sk_xprt);
	struct sockaddr_storage addr;
	struct sockaddr	*sin = (struct sockaddr *)&addr;
	struct svc_serv	*serv = svsk->sk_xprt.xpt_server;
	struct socket	*sock = svsk->sk_sock;
	struct socket	*newsock;
	struct svc_sock	*newsvsk;
	int		err, slen;

	if (!sock)
		return NULL;

	clear_bit(XPT_CONN, &svsk->sk_xprt.xpt_flags);
	err = kernel_accept(sock, &newsock, O_NONBLOCK);
	if (err < 0) {
		if (err == -ENOMEM)
			printk(KERN_WARNING "%s: no more sockets!\n",
			       serv->sv_name);
		else if (err != -EAGAIN)
			net_warn_ratelimited("%s: accept failed (err %d)!\n",
					     serv->sv_name, -err);
		trace_svcsock_accept_err(xprt, serv->sv_name, err);
		return NULL;
	}
	set_bit(XPT_CONN, &svsk->sk_xprt.xpt_flags);

	err = kernel_getpeername(newsock, sin);
	if (err < 0) {
		trace_svcsock_getpeername_err(xprt, serv->sv_name, err);
		goto failed;
	}
	slen = err;

	/* Reset the inherited callbacks before calling svc_setup_socket */
	newsock->sk->sk_state_change = svsk->sk_ostate;
	newsock->sk->sk_data_ready = svsk->sk_odata;
	newsock->sk->sk_write_space = svsk->sk_owspace;

	/* make sure that a write doesn't block forever when
	 * low on memory
	 */
	newsock->sk->sk_sndtimeo = HZ*30;

	newsvsk = svc_setup_socket(serv, newsock,
				 (SVC_SOCK_ANONYMOUS | SVC_SOCK_TEMPORARY));
	if (IS_ERR(newsvsk))
		goto failed;
	svc_xprt_set_remote(&newsvsk->sk_xprt, sin, slen);
	err = kernel_getsockname(newsock, sin);
	slen = err;
	if (unlikely(err < 0))
		slen = offsetof(struct sockaddr, sa_data);
	svc_xprt_set_local(&newsvsk->sk_xprt, sin, slen);

	if (sock_is_loopback(newsock->sk))
		set_bit(XPT_LOCAL, &newsvsk->sk_xprt.xpt_flags);
	else
		clear_bit(XPT_LOCAL, &newsvsk->sk_xprt.xpt_flags);
	if (serv->sv_stats)
		serv->sv_stats->nettcpconn++;

	return &newsvsk->sk_xprt;

failed:
	sock_release(newsock);
	return NULL;
}
```

[struct svc_sock — struct — include/linux/sunrpc/svcsock.h:26-49]
```c
struct svc_sock {
	struct svc_xprt		sk_xprt;
	struct socket *	sk_sock;	/* berkeley socket layer */
	struct sock *	sk_sk;		/* INET layer */

	/* We keep the old state_change and data_ready CB's here */
	void			(*sk_ostate)(struct sock *);
	void			(*sk_odata)(struct sock *);
	void			(*sk_owspace)(struct sock *);

	/* private TCP part */
	/* On-the-wire fragment header: */
	__be32			sk_marker;
	/* As we receive a record, this includes the length received so
	 * far (including the fragment header): */
	u32			sk_tcplen;
	/* Total length of the data (not including fragment headers)
	 * received so far in the fragments making up this rpc: */
	u32			sk_datalen;

	struct page *	sk_pages[RPCSVC_MAXPAGES];	/* received data */
};
```

[XPT_CONN — constant — include/linux/sunrpc/svc_xprt.h:68]
XPT_CONN → 1 /* conn pending */  (include/linux/sunrpc/svc_xprt.h:68)

[TCP_LISTEN — constant — include/net/tcp_states.h]
TCP_LISTEN → 10  (include/net/tcp_states.h)

[sk_user_data — field — include/net/sock.h]
void __user *sk_user_data;

[rmb — macro — include/linux/compiler.h]
rmb → #define rmb() barrier()  (include/linux/compiler.h)

Control flow: The function extracts a pointer to svc_sock from sk->sk_user_data. It immediately evaluates sk->sk_state, returning early if the socket is not in the TCP_LISTEN state. If the state check passes, it verifies that the extracted svc_sock pointer is non-NULL. Within the guarded block, it executes a read memory barrier (rmb()), invokes the stored sk_odata callback, sets the XPT_CONN flag on the transport structure, and passes the transport to svc_xprt_enqueue. The function contains no explicit error handling paths as it is a void kernel callback, relying entirely on early returns for invalid states or pointers.

Data flow: Input sk (struct sock) provides sk->sk_state and sk->sk_user_data. sk_user_data is cast to svc_sock * and bound to svsk. The state check filters out sockets not in TCP_LISTEN (e.g., child sockets that inherited the callback). If valid, svsk is dereferenced to read the sk_odata function pointer and sk_xprt structure. sk_odata is invoked with sk. The XPT_CONN bit is modified, and svsk->sk_xprt is queued. The rmb() barrier ensures that loads of svsk fields occur after the pointer load, synchronizing with the wmb() used during socket initialization.

Verdict: Not Vulnerable

Reviewed region:
[net/sunrpc/svcsock.c]
```c
static void svc_tcp_listen_data_ready(struct sock *sk)
{
	struct svc_sock	*svsk = (struct svc_sock *)sk->sk_user_data;

	/*
	 * This callback may called twice when a new connection
	 * is established as a child socket inherits everything
	 * from a parent LISTEN socket.
	 * 1) data_ready method of the parent socket will be called
	 *    when one of child sockets become ESTABLISHED.
	 * 2) data_ready method of the child socket may be called
	 *    when it receives data before the socket is accepted.
	 * In case of 2, we should ignore it silently and DO NOT
	 * dereference svsk.
	 */
	if (sk->sk_state != TCP_LISTEN)
		return;

	if (svsk) {
		/* Refer to svc_setup_socket() for details. */
		rmb();
		svsk->sk_odata(sk);
		set_bit(XPT_CONN, &svsk->sk_xprt.xpt_flags);
		svc_xprt_enqueue(&svsk->sk_xprt);
	}
}
```

Why it is not vulnerable: The function enforces strict state and pointer validation before any dereference. The check if (sk->sk_state != TCP_LISTEN) return; ensures the callback only operates on legitimate listener sockets, safely ignoring inherited callbacks on child sockets that may carry stale or mismatched lifecycle pointers. The subsequent if (svsk) guard prevents null or use-after-free dereferences. The rmb() barrier guarantees safe publication of struct fields established by wmb() in svc_setup_socket. Together, these controls definitively prevent unsafe pointer access.

Security invariants:
- Socket state must equal TCP_LISTEN to prevent processing inherited callbacks on child sockets with potentially stale pointers. Enforced by: if (sk->sk_state != TCP_LISTEN) return;
- The svc_sock pointer must be non-NULL before dereferencing to avoid null dereferences or use-after-free on detached sockets. Enforced by: if (svsk) { ... }
- Memory ordering must guarantee that svc_sock fields are fully initialized before access, matching the store ordering during setup. Enforced by: rmb() placed before accessing svsk->sk_odata.
- Concurrent detachment must safely clear the user data pointer, which the callback handles via the null check. Enforced by: svc_sock_detach setting sk->sk_user_data = NULL under lock_sock(sk), caught by the if (svsk) guard.

Invariant verification:
- State validation filters inherited child sockets: holds=true. Evidence: if (sk->sk_state != TCP_LISTEN) return; explicitly returns for non-listener sockets, preventing callback execution on child sockets that inherit the callback but transition to states like TCP_ESTABLISHED.
- Null pointer check prevents dereference of stale/cleared pointers: holds=true. Evidence: if (svsk) { ... } guards all dereferences of svsk, safely handling cases where sk_user_data has been cleared or was never set.
- Memory barrier ensures safe publication of struct fields: holds=true. Evidence: rmb() is called immediately after extracting svsk and before accessing svsk->sk_odata, correctly pairing with the wmb() in svc_setup_socket before inet->sk_user_data = svsk;
- Detachment safely clears pointers under lock: holds=true. Evidence: svc_sock_detach sets sk->sk_user_data = NULL while holding lock_sock(sk), and the null check in this callback safely handles the cleared state without requiring the lock.

Residual risk notes: The function relies on the networking stack maintaining sk->sk_state consistently during socket lifecycle transitions. While the null check and state check provide robust safeguards, the callback does not acquire lock_sock(sk) itself, relying instead on the memory barrier and atomic state flags for synchronization. This design is consistent with kernel socket callback conventions and presents no concrete vulnerability in the supplied context.

## Analysis

### Function purpose
`svc_tcp_listen_data_ready` is the `sk_data_ready` callback installed on a listening TCP socket by `svc_setup_socket`. When a child socket inherits the listening parent's callbacks, this function may be invoked on the child before `svc_tcp_accept` resets the callbacks. The early `if (sk->sk_state != TCP_LISTEN) return;` guard exists precisely to handle that case safely.

### Suspected concern via the call path
The call path goes through teardown:
- `svc_xprt_destroy_all` → `svc_xprt_free` → `svc_sock_free`

A concern would be: if `svc_sock_detach` (called from `svc_xprt_free` typically) restores the original callbacks and then `svc_sock_free` releases the `svc_sock`, could a concurrent invocation of `svc_tcp_listen_data_ready` race and dereference a freed `svsk` via `sk_user_data`?

### Guards observed
1. `svc_sock_detach` clears `sk->sk_user_data = NULL` under `lock_sock(sk)` and restores the original callbacks.
2. `svc_tcp_listen_data_ready` reads `svsk = sk->sk_user_data` and tests `if (svsk)` before dereferencing.
3. The `rmb()` pairs with the `wmb()` in `svc_setup_socket` (ensuring `sk_odata` is visible before `sk_user_data` is published).
4. The `TCP_LISTEN` state check filters out child-socket inheritance scenarios.

### Data-flow reasoning on free
- `svc_sock_free` only runs after `svc_xprt_free`, which calls the transport's `xpo_detach` (i.e., `svc_sock_detach`). Detach takes `lock_sock`, sets `sk_user_data = NULL` and restores the original `sk_data_ready`. After `release_sock`, no new invocation of `svc_tcp_listen_data_ready` will be entered for this socket because the kernel will dispatch to the restored callback. Any in-flight invocation of `svc_tcp_listen_data_ready` already running before detach completes would have to be serialized against `lock_sock`, but `data_ready` is typically called under socket lock or with bh-disable; in any case the read of `sk_user_data` happens before detach replaces it.
- Even if `svsk` were observed as non-NULL during the brief race, the listen socket's `svc_sock` is not freed until after detach + sock release, and the ordering provided by `lock_sock`/`release_sock` in detach plus the `if (svsk)` null check together with the `TCP_LISTEN` state check provide safety.

### Other angles
- `svsk->sk_odata(sk)` invokes the saved original `sk_data_ready`. This is the standard chain pattern; safe because `sk_odata` was captured before publishing `sk_user_data` (with `wmb()`/`rmb()` ordering).
- Setting `XPT_CONN` and calling `svc_xprt_enqueue` are normal listen-handler operations.
- The comment about double invocation on child sockets is addressed by the `TCP_LISTEN` check; child sockets are in `TCP_SYN_RECV`/`TCP_ESTABLISHED`, so the function returns before touching `svsk`.

### No evidence of a bug
The function's logic — state check, NULL check, read-barrier, then dereference — matches the pairing in `svc_setup_socket` (wmb before publishing) and `svc_sock_detach` (clearing under socket lock). The teardown path goes through `svc_sock_detach` before `svc_sock_free`, so by the time the svc_sock is freed, the callback has been replaced with the original and no new callback dispatch will reach `svc_tcp_listen_data_ready`.

### Residual uncertainty
The supplied context does not show `svc_xprt_free`/`svc_sock_free` bodies, so the exact ordering between `svc_sock_detach` and `kfree(svsk)` cannot be verified directly here. However, the standard kernel pattern (and the existence of `svc_sock_detach` with this exact intent) supports the conclusion. Also, RCU/synchronize requirements between detach and free are not visible, but data_ready callbacks run under the socket's lock context which detach also takes, providing the needed serialization.

## Verdict

**Not vulnerable** based on the supplied code.

The function correctly:
- Filters non-listen states (handles child-socket inheritance).
- Null-checks `sk_user_data`.
- Uses `rmb()` paired with the `wmb()` in `svc_setup_socket` to ensure `sk_odata` is observed consistently with `sk_user_data` publication.
- Relies on `svc_sock_detach`'s `lock_sock`-protected restoration of original callbacks and clearing of `sk_user_data` to prevent use-after-free during teardown via `svc_sock_free`.

No realistic UAF, NULL-deref, or race exploitable from the given call path is demonstrated by the supplied evidence.

net/sunrpc/svcsock.c svc_tcp_listen_data_ready; with cross-references to svc_setup_socket, svc_sock_detach, svc_tcp_accept

No exploitable path is demonstrated by the supplied evidence. The teardown sequence svc_xprt_free → svc_sock_detach restores the original sk_data_ready and clears sk_user_data under lock_sock before svc_sock_free releases the structure, so no new invocation of svc_tcp_listen_data_ready can dereference a freed svsk.

None demonstrated. A hypothetical race would require concurrent dispatch of the data_ready callback after the svc_sock is freed, which is precluded by detach replacing the callback under socket lock prior to free.

The handler enforces multiple guards: (1) sk_state == TCP_LISTEN check filters child sockets that inherit the listener's callbacks, preventing dereference in the inheritance window; (2) explicit NULL-check of svsk = sk_user_data; (3) rmb() pairs with wmb() in svc_setup_socket, ensuring sk_odata is observed consistently with the publication of sk_user_data; (4) svc_sock_detach restores the original sk_data_ready and clears sk_user_data under lock_sock/release_sock prior to svc_sock_free, so by the time svsk is freed, this function is no longer reachable as the socket's data_ready callback.

sk_user_data publication is ordered after sk_odata initialization (wmb in svc_setup_socket / rmb here). Callback replacement and sk_user_data clearing in svc_sock_detach occur under lock_sock before svc_sock_free. Listener-only logic guarded by TCP_LISTEN state check to handle child-socket inheritance.

Verified from svc_setup_socket (wmb before inet->sk_user_data = svsk), svc_sock_detach (lock_sock; restore callbacks; sk_user_data=NULL; release_sock), and the in-function rmb plus state and NULL checks. Exact body of svc_sock_free is not shown, but the standard ordering (detach before free) is implied by the xprt teardown contract.

svc_xprt_free and svc_sock_free bodies are not provided; if a future change frees svsk without first invoking svc_sock_detach, or without sufficient synchronize/RCU between detach and free, a UAF could re-emerge. Also assumes data_ready dispatch is serialized with lock_sock as is standard in the kernel sock layer.

nfsd_svc → write_threads → nfsd_put → svc_xprt_destroy_all → svc_xprt_free → svc_sock_free → svc_tcp_listen_data_ready


Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/stable/linux
Language: C
File: fs/ext4/namei.c
Function: make_indexed_dir

Call path: do_sys_openat2 (fs/open.c) → path_openat (fs/namei.c) → ext4_create (fs/ext4/namei.c) → ext4_add_nondir (fs/ext4/namei.c) → ext4_add_entry (fs/ext4/namei.c) → make_indexed_dir (fs/ext4/namei.c)

### Primary Function

```c
static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
			    struct inode *dir,
			    struct inode *inode, struct buffer_head *bh)
{
	struct buffer_head *bh2;
	struct dx_root	*root;
	struct dx_frame	frames[EXT4_HTREE_LEVEL], *frame;
	struct dx_entry *entries;
	struct ext4_dir_entry_2	*de, *de2;
	char		*data2, *top;
	unsigned	len;
	int		retval;
	unsigned	blocksize;
	ext4_lblk_t  block;
	struct fake_dirent *fde;
	int csum_size = 0;

	if (ext4_has_metadata_csum(inode->i_sb))
		csum_size = sizeof(struct ext4_dir_entry_tail);

	blocksize =  dir->i_sb->s_blocksize;
	dxtrace(printk(KERN_DEBUG "Creating index: inode %lu\n", dir->i_ino));
	BUFFER_TRACE(bh, "get_write_access");
	retval = ext4_journal_get_write_access(handle, bh);
	if (retval) {
		ext4_std_error(dir->i_sb, retval);
		brelse(bh);
		return retval;
	}
	root = (struct dx_root *) bh->b_data;

	/* The 0th block becomes the root, move the dirents out */
	fde = &root->dotdot;
	de = (struct ext4_dir_entry_2 *)((char *)fde +
		ext4_rec_len_from_disk(fde->rec_len, blocksize));
	if ((char *) de >= (((char *) root) + blocksize)) {
		EXT4_ERROR_INODE(dir, "invalid rec_len for '..'");
		brelse(bh);
		return -EFSCORRUPTED;
	}
	len = ((char *) root) + (blocksize - csum_size) - (char *) de;

	/* Allocate new block for the 0th block's dirents */
	bh2 = ext4_append(handle, dir, &block);
	if (IS_ERR(bh2)) {
		brelse(bh);
		return PTR_ERR(bh2);
	}
	ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
	data2 = bh2->b_data;

	memcpy(data2, de, len);
	de = (struct ext4_dir_entry_2 *) data2;
	top = data2 + len;
	while ((char *)(de2 = ext4_next_entry(de, blocksize)) < top) {
		if (ext4_check_dir_entry(dir, NULL, de, bh2, data2, len,
					 (data2 + (blocksize - csum_size) -
					  (char *) de))) {
			brelse(bh2);
			brelse(bh);
			return -EFSCORRUPTED;
		}
		de = de2;
	}
	de->rec_len = ext4_rec_len_to_disk(data2 + (blocksize - csum_size) -
					   (char *) de, blocksize);

	if (csum_size)
		ext4_initialize_dirent_tail(bh2, blocksize);

	/* Initialize the root; the dot dirents already exist */
	de = (struct ext4_dir_entry_2 *) (&root->dotdot);
	de->rec_len = ext4_rec_len_to_disk(blocksize - EXT4_DIR_REC_LEN(2),
					   blocksize);
	memset (&root->info, 0, sizeof(root->info));
	root->info.info_length = sizeof(root->info);
	root->info.hash_version = EXT4_SB(dir->i_sb)->s_def_hash_version;
	entries = root->entries;
	dx_set_block(entries, 1);
	dx_set_count(entries, 1);
	dx_set_limit(entries, dx_root_limit(dir, sizeof(root->info)));

	/* Initialize as for dx_probe */
	fname->hinfo.hash_version = root->info.hash_version;
	if (fname->hinfo.hash_version <= DX_HASH_TEA)
		fname->hinfo.hash_version += EXT4_SB(dir->i_sb)->s_hash_unsigned;
	fname->hinfo.seed = EXT4_SB(dir->i_sb)->s_hash_seed;
	ext4fs_dirhash(dir, fname_name(fname), fname_len(fname), &fname->hinfo);

	memset(frames, 0, sizeof(frames));
	frame = frames;
	frame->entries = entries;
	frame->at = entries;
	frame->bh = bh;

	retval = ext4_handle_dirty_dx_node(handle, dir, frame->bh);
	if (retval)
		goto out_frames;
	retval = ext4_handle_dirty_dirblock(handle, dir, bh2);
	if (retval)
		goto out_frames;

	de = do_split(handle,dir, &bh2, frame, &fname->hinfo);
	if (IS_ERR(de)) {
		retval = PTR_ERR(de);
		goto out_frames;
	}

	retval = add_dirent_to_buf(handle, fname, dir, inode, de, bh2);
out_frames:
	/*
	 * Even if the block split failed, we have to properly write
	 * out all the changes we did so far. Otherwise we can end up
	 * with corrupted filesystem.
	 */
	if (retval)
		ext4_mark_inode_dirty(handle, dir);
	dx_release(frames);
	brelse(bh2);
	return retval;
}
```

### Cross-File Context

[ext4_next_entry — function — fs/ext4/namei.c:551-555]
```c
static inline struct ext4_dir_entry_2 *
ext4_next_entry(struct ext4_dir_entry_2 *p, unsigned long blocksize)
{
	return (struct ext4_dir_entry_2 *)((char *)p +
		ext4_rec_len_from_disk(p->rec_len, blocksize));
}
```

[ext4_rec_len_from_disk — function — fs/ext4/ext4.h:2232-2242]
```c
static inline unsigned int
ext4_rec_len_from_disk(__le16 dlen, unsigned blocksize)
{
	unsigned len = le16_to_cpu(dlen);

#if (PAGE_SIZE >= 65536)
	if (len == EXT4_MAX_REC_LEN || len == 0)
		return blocksize;
	return (len & 65532) | ((len & 3) << 16);
#else
	return len;
#endif
}
```

[ext4_rec_len_to_disk — function — fs/ext4/ext4.h:2245-2262]
```c
static inline __le16 ext4_rec_len_to_disk(unsigned len, unsigned blocksize)
{
	if ((len > blocksize) || (blocksize > (1 << 18)) || (len & 3))
		BUG();
#if (PAGE_SIZE >= 65536)
	if (len < 65536)
		return cpu_to_le16(len);
	if (len == blocksize) {
		if (blocksize == 65536)
			return cpu_to_le16(EXT4_MAX_REC_LEN);
		else
			return cpu_to_le16(0);
	}
	return cpu_to_le16((len & 65532) | ((len >> 16) & 3));
#else
	return cpu_to_le16(len);
#endif
}
```

[ext4_check_dir_entry — function — fs/ext4/dir.c:66-108]
```c
int __ext4_check_dir_entry(const char *function, unsigned int line,
			   struct inode *dir, struct file *filp,
			   struct ext4_dir_entry_2 *de,
			   struct buffer_head *bh, char *buf, int size,
			   unsigned int offset)
{
	const char *error_msg = NULL;
	const int rlen = ext4_rec_len_from_disk(de->rec_len,
						dir->i_sb->s_blocksize);
	const int next_offset = ((char *) de - buf) + rlen;

	if (unlikely(rlen < EXT4_DIR_REC_LEN(1)))
		error_msg = "rec_len is smaller than minimal";
	else if (unlikely(rlen % 4 != 0))
		error_msg = "rec_len % 4 != 0";
	else if (unlikely(rlen < EXT4_DIR_REC_LEN(de->name_len)))
		error_msg = "rec_len is too small for name_len";
	else if (unlikely(next_offset > size))
		error_msg = "directory entry overrun";
	else if (unlikely(next_offset > size - EXT4_DIR_REC_LEN(1) &&
			  next_offset != size))
		error_msg = "directory entry too close to block end";
	else if (unlikely(le32_to_cpu(de->inode) >
			le32_to_cpu(EXT4_SB(dir->i_sb)->s_es->s_inodes_count)))
		error_msg = "inode out of bounds";
	else
		return 0;

	if (filp)
		ext4_error_file(filp, function, line, bh->b_blocknr,
			"bad entry in directory: %s - offset=%u, "
			"inode=%u, rec_len=%d, name_len=%d, size=%d",
			error_msg, offset, le32_to_cpu(de->inode),
			rlen, de->name_len, size);
	else
		ext4_error_inode(dir, function, line, bh->b_blocknr,
			 "bad entry in directory: %s - offset=%u, "
			 "inode=%u, rec_len=%d, name_len=%d, size=%d",
			  error_msg, offset, le32_to_cpu(de->inode),
			  rlen, de->name_len, size);

	return 1;
}
```

[ext4_dir_entry_2 — struct — fs/ext4/ext4.h:2174-2180]
```c
struct ext4_dir_entry_2 {
	__le32	inode;		/* Inode number */
	__le16	rec_len;		/* Directory entry length */
	__u8	name_len;		/* Name length */
	__u8	file_type;		/* See file type macros EXT4_FT_* below */
	char	name[EXT4_NAME_LEN];	/* File name */
};
```

[fake_dirent — struct — fs/ext4/namei.c:218-224]
```c
struct fake_dirent
{
	__le32 inode;
	__le16 rec_len;
	u8 name_len;
	u8 file_type;
};
```

[EXT4_DIR_REC_LEN — macro — fs/ext4/ext4.h:2223-2224]
EXT4_DIR_REC_LEN → #define EXT4_DIR_REC_LEN(name_len) (((name_len) + 8 + EXT4_DIR_ROUND) & \ ~EXT4_DIR_ROUND)  (fs/ext4/ext4.h:2223-2224)

[EXT4_DIR_ROUND — constant — fs/ext4/ext4.h:2221-2222]
EXT4_DIR_ROUND → #define EXT4_DIR_PAD 4 #define EXT4_DIR_ROUND (EXT4_DIR_PAD - 1)  (fs/ext4/ext4.h:2221-2222)

Control flow: make_indexed_dir converts a linear directory into an htree-indexed directory. It first validates the dotdot entry offset stays within the block. It allocates a new block via ext4_append, copies existing directory entries from the root block to the new block, and iterates through them. During iteration, ext4_next_entry computes the next entry pointer, and ext4_check_dir_entry validates each entry before the pointer advances. The function then initializes the htree root structure, calls do_split to insert the new entry, and adds the directory entry via add_dirent_to_buf. On any error, cleanup paths release buffers (bh, bh2) via brelse, release dx frames via dx_release, and mark the inode dirty before returning the error code.

Data flow: The function receives the current buffer head (bh) containing the root block, a filename, and inodes. It extracts the dotdot entry, computes the start offset of subsequent entries using ext4_rec_len_from_disk(fde->rec_len, blocksize), and validates that the resulting pointer stays within the block. The valid data region (len) is calculated and copied to a new block (bh2). During iteration, each entry's rec_len is consumed by ext4_next_entry via ext4_rec_len_from_disk, and validated by ext4_check_dir_entry which checks rlen >= EXT4_DIR_REC_LEN(1), rlen % 4 == 0, and next_offset <= size. The last entry's rec_len is updated via ext4_rec_len_to_disk to fill remaining space. The root block is restructured with htree metadata, and the hash information is populated via ext4fs_dirhash before do_split processes the tree insertion.

Verdict: Not Vulnerable

Reviewed region:
[fs/ext4/namei.c]
```c
static int make_indexed_dir(handle_t *handle, struct ext4_filename *fname,
			    struct inode *dir,
			    struct inode *inode, struct buffer_head *bh)
{
	struct buffer_head *bh2;
	struct dx_root	*root;
	struct dx_frame	frames[EXT4_HTREE_LEVEL], *frame;
	struct dx_entry *entries;
	struct ext4_dir_entry_2	*de, *de2;
	char		*data2, *top;
	unsigned	len;
	int		retval;
	unsigned	blocksize;
	ext4_lblk_t  block;
	struct fake_dirent *fde;
	int csum_size = 0;

	if (ext4_has_metadata_csum(inode->i_sb))
		csum_size = sizeof(struct ext4_dir_entry_tail);

	blocksize =  dir->i_sb->s_blocksize;
	dxtrace(printk(KERN_DEBUG "Creating index: inode %lu\n", dir->i_ino));
	BUFFER_TRACE(bh, "get_write_access");
	retval = ext4_journal_get_write_access(handle, bh);
	if (retval) {
		ext4_std_error(dir->i_sb, retval);
		brelse(bh);
		return retval;
	}
	root = (struct dx_root *) bh->b_data;

	/* The 0th block becomes the root, move the dirents out */
	fde = &root->dotdot;
	de = (struct ext4_dir_entry_2 *)((char *)fde +
		ext4_rec_len_from_disk(fde->rec_len, blocksize));
	if ((char *) de >= (((char *) root) + blocksize)) {
		EXT4_ERROR_INODE(dir, "invalid rec_len for '..'");
		brelse(bh);
		return -EFSCORRUPTED;
	}
	len = ((char *) root) + (blocksize - csum_size) - (char *) de;

	/* Allocate new block for the 0th block's dirents */
	bh2 = ext4_append(handle, dir, &block);
	if (IS_ERR(bh2)) {
		brelse(bh);
		return PTR_ERR(bh2);
	}
	ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
	data2 = bh2->b_data;

	memcpy(data2, de, len);
	de = (struct ext4_dir_entry_2 *) data2;
	top = data2 + len;
	while ((char *)(de2 = ext4_next_entry(de, blocksize)) < top) {
		if (ext4_check_dir_entry(dir, NULL, de, bh2, data2, len,
					 (data2 + (blocksize - csum_size) -
					  (char *) de))) {
			brelse(bh2);
			brelse(bh);
			return -EFSCORRUPTED;
		}
		de = de2;
	}
	de->rec_len = ext4_rec_len_to_disk(data2 + (blocksize - csum_size) -
					   (char *) de, blocksize);

	if (csum_size)
		ext4_initialize_dirent_tail(bh2, blocksize);

	/* Initialize the root; the dot dirents already exist */
	de = (struct ext4_dir_entry_2 *) (&root->dotdot);
	de->rec_len = ext4_rec_len_to_disk(blocksize - EXT4_DIR_REC_LEN(2),
					   blocksize);
	memset (&root->info, 0, sizeof(root->info));
	root->info.info_length = sizeof(root->info);
	root->info.hash_version = EXT4_SB(dir->i_sb)->s_def_hash_version;
	entries = root->entries;
	dx_set_block(entries, 1);
	dx_set_count(entries, 1);
	dx_set_limit(entries, dx_root_limit(dir, sizeof(root->info)));

	/* Initialize as for dx_probe */
	fname->hinfo.hash_version = root->info.hash_version;
	if (fname->hinfo.hash_version <= DX_HASH_TEA)
		fname->hinfo.hash_version += EXT4_SB(dir->i_sb)->s_hash_unsigned;
	fname->hinfo.seed = EXT4_SB(dir->i_sb)->s_hash_seed;
	ext4fs_dirhash(dir, fname_name(fname), fname_len(fname), &fname->hinfo);

	memset(frames, 0, sizeof(frames));
	frame = frames;
	frame->entries = entries;
	frame->at = entries;
	frame->bh = bh;

	retval = ext4_handle_dirty_dx_node(handle, dir, frame->bh);
	if (retval)
		goto out_frames;
	retval = ext4_handle_dirty_dirblock(handle, dir, bh2);
	if (retval)
		goto out_frames;

	de = do_split(handle,dir, &bh2, frame, &fname->hinfo);
	if (IS_ERR(de)) {
		retval = PTR_ERR(de);
		goto out_frames;
	}

	retval = add_dirent_to_buf(handle, fname, dir, inode, de, bh2);
out_frames:
	/*
	 * Even if the block split failed, we have to properly write
	 * out all the changes we did so far. Otherwise we can end up
	 * with corrupted filesystem.
	 */
	if (retval)
		ext4_mark_inode_dirty(handle, dir);
	dx_release(frames);
	brelse(bh2);
	return retval;
}
```

Why it is not vulnerable: The directory entry iteration loop in make_indexed_dir is properly protected against invalid record lengths. Each entry is validated by ext4_check_dir_entry before the iteration pointer advances. ext4_check_dir_entry enforces that rec_len >= EXT4_DIR_REC_LEN(1) (minimum 12 bytes), rec_len % 4 == 0 (alignment), and next_offset <= size (no overrun past the block). The loop condition also independently bounds iteration by checking that ext4_next_entry's result pointer is below 'top', which is set to data2 + len. The initial bounds check validates that the first directory entry pointer does not exceed the block boundary. These layered checks ensure that no matter what rec_len values appear on disk, the iteration cannot read out-of-bounds or loop indefinitely.

Security invariants:
- Initial directory entry offset must not exceed block boundary, enforced by the check: if ((char *) de >= (((char *) root) + blocksize)) returning -EFSCORRUPTED
- Record length must be a multiple of 4 for proper alignment, enforced by ext4_check_dir_entry: rlen % 4 != 0 returns error
- Record length must be at least the minimum directory entry size, enforced by ext4_check_dir_entry: rlen < EXT4_DIR_REC_LEN(1) returns error
- Next entry offset must not exceed the valid block data size, enforced by ext4_check_dir_entry: next_offset > size returns error with message 'directory entry overrun'
- Loop iteration must terminate within the valid data region, enforced by the loop condition: (char *)(de2 = ext4_next_entry(de, blocksize)) < top where top = data2 + len
- Entry record length must be sufficient to contain the declared name, enforced by ext4_check_dir_entry: rlen < EXT4_DIR_REC_LEN(de->name_len) returns error

Invariant verification:
- Bounds validation of initial directory entry pointer before copying data: holds=true. Evidence: The check 'if ((char *) de >= (((char *) root) + blocksize)) { EXT4_ERROR_INODE(dir, "invalid rec_len for '..'"); brelse(bh); return -EFSCORRUPTED; }' ensures de does not point beyond the block, even if fde->rec_len contains corrupted values
- Record length alignment to 4-byte boundary during iteration: holds=true. Evidence: ext4_check_dir_entry checks 'rlen % 4 != 0' and returns an error with message 'rec_len % 4 != 0', preventing misaligned pointer arithmetic in ext4_next_entry
- Minimum record length enforcement prevents zero-length or undersized entries: holds=true. Evidence: ext4_check_dir_entry checks 'rlen < EXT4_DIR_REC_LEN(1)' returning error 'rec_len is smaller than minimal', ensuring ext4_next_entry always advances by at least 12 bytes
- Next entry offset does not exceed block boundary: holds=true. Evidence: ext4_check_dir_entry computes 'next_offset = ((char *) de - buf) + rlen' and checks 'next_offset > size' returning error 'directory entry overrun'. The size parameter passed is '(data2 + (blocksize - csum_size) - (char *) de)' representing remaining valid space
- Loop termination guaranteed by upper bound check on next pointer: holds=true. Evidence: The while loop condition '(char *)(de2 = ext4_next_entry(de, blocksize)) < top' ensures de2 is within valid range before ext4_check_dir_entry is called; if rec_len is large enough to push de2 beyond top, the loop terminates without processing invalid entries
- Journal write access acquired before modifying block data: holds=true. Evidence: ext4_journal_get_write_access(handle, bh) is called with error checking: 'if (retval) { ext4_std_error(dir->i_sb, retval); brelse(bh); return retval; }'
- Buffer resources released on all error paths: holds=true. Evidence: Every error return path calls brelse(bh2) and brelse(bh); the out_frames label ensures dx_release(frames) and brelse(bh2) are called, and all return paths lead through this cleanup

Residual risk notes: The validation in ext4_check_dir_entry covers the primary safety concerns for record length iteration. One minor consideration is that ext4_rec_len_from_disk on systems with PAGE_SIZE >= 65536 can produce values exceeding blocksize from certain 16-bit patterns (e.g., 0x3FFF yields 262140), but such values would cause ext4_next_entry to produce a de2 pointer far beyond 'top', causing immediate loop termination via the 'de2 < top' check before any dereference of the resulting entry occurs. The ext4_rec_len_to_disk function contains a BUG() assertion for len > blocksize, but the computed lengths in this code path are always bounded by (blocksize - csum_size) since de iterates from data2 toward data2 + len.

## Analysis

### Control flow

`make_indexed_dir` converts a linear directory into an indexed (htree) directory. It:

1. Reads `..` from the root block, computes `de` (the entry after `..`).
2. Validates `de < root + blocksize`.
3. Allocates a new block, copies `[de, root+blocksize-csum_size)` (length `len`) into the new block.
4. Walks the copied entries via `ext4_next_entry`, and for each calls `ext4_check_dir_entry` to validate.

### Suspect code: the validation loop

```c
de = (struct ext4_dir_entry_2 *) data2;
top = data2 + len;
while ((char *)(de2 = ext4_next_entry(de, blocksize)) < top) {
    if (ext4_check_dir_entry(dir, NULL, de, bh2, data2, len,
                 (data2 + (blocksize - csum_size) - (char *) de))) {
        ...
        return -EFSCORRUPTED;
    }
    de = de2;
}
de->rec_len = ext4_rec_len_to_disk(data2 + (blocksize - csum_size) -
                   (char *) de, blocksize);
```

Two concerns:

**(1) `de2 = ext4_next_entry(de, blocksize)` is computed before `de` is validated.**
The loop advances by reading `de->rec_len` *before* `ext4_check_dir_entry(de)` runs. If `de->rec_len` is corrupt (e.g., zero or huge), `de2` could be an arbitrary pointer. However, the comparison `(char *)de2 < top` and the subsequent `ext4_check_dir_entry` does verify `next_offset <= size`. But `next_offset = (de - buf) + rlen`, where buf=data2, size=len. So if rlen is bogus, next_offset overflows and is caught.

Wait — the check happens *after* we read `de->rec_len`. Reading the field is safe (we're inside the buffer for the first iteration). The issue is whether `ext4_next_entry` could push the pointer outside the buffer making the `< top` comparison meaningless... but pointer comparison is fine, and we then call ext4_check_dir_entry on `de` (current), which validates rlen. If rlen is bad, error returned. So the loop *does* validate each `de` before using `de2`.

But: when `(char *)de2 >= top`, the loop exits without ever validating the *last* entry `de`. That's the unvalidated final entry.

**(2) Final entry not validated, then `rec_len` is overwritten.**

After the loop, the code unconditionally sets:
```c
de->rec_len = ext4_rec_len_to_disk(data2 + (blocksize - csum_size) - (char *) de, blocksize);
```

The argument is `data2 + (blocksize - csum_size) - (char *)de`. For this to be valid, `de` must point inside `[data2, data2 + blocksize - csum_size]`.

Consider attacker-controlled (or corrupted on-disk) `..` entry with a `rec_len` such that `de` (the entry after `..`) lies just inside the block, but its own `rec_len` is, say, larger than blocksize. Actually wait — `de` here refers to the moved entries in `data2`, and the loop walks them. If the very first `de` (start of data2) has `rec_len` such that `ext4_next_entry(de) >= top` immediately, the loop body never executes — `de` is unvalidated. Then `ext4_rec_len_to_disk` is called with `len = data2 + (blocksize-csum_size) - (char*)de = blocksize - csum_size`, which is fine for `to_disk` (no BUG).

But there is a real concern: the final `de` might have `(char*)de` such that `data2 + (blocksize-csum_size) - (char*)de` is not a multiple of 4 or is > blocksize. For `de` to land beyond `data2 + blocksize - csum_size`, you'd need previous entries to have advanced past that. The loop's `< top` check ensures `de2 < data2 + len = data2 + blocksize - csum_size` before becoming the new `de`. Wait — actually:

`top = data2 + len` where `len = blocksize - csum_size - (de - root)` originally... no:

```c
len = ((char *) root) + (blocksize - csum_size) - (char *) de;
```
This is the source `len` (size of region copied). Then `data2` gets `len` bytes. So `top = data2 + len`, and `len <= blocksize - csum_size`.

The final `de` satisfies `de < top` (it was either the initial `data2` or set from a `de2` that satisfied `< top`). So `data2 + len - (char*)de > 0`. Then `ext4_rec_len_to_disk(len_remaining, blocksize)`. But `len_remaining` is `data2 + (blocksize - csum_size) - de`, not `data2 + len - de`. These differ when `len < blocksize - csum_size`, i.e., when csum_size==0 they're equal; when csum_size!=0, `len = blocksize - csum_size - (de_initial - root)`. Hmm actually the original `de` in root is at offset `EXT4_DIR_REC_LEN(1) + ..rec_len`, often offset 24 (for "."+".."), so `len = blocksize - csum_size - 24` typically. Then `data2 + (blocksize - csum_size) - de` is *larger* than `data2 + len - de` by ~24 bytes → that's beyond `top`!

Wait — let me re-read:

```c
len = ((char *) root) + (blocksize - csum_size) - (char *) de;  // de here is in root
```
Where `de` = `fde + rec_len(fde)` = address of first entry after "..". Typically that's offset 24. So `len = blocksize - csum_size - 24`.

Then `data2` (start of new block at offset 0) holds the copied `len` bytes (offsets 0..len in data2 contain content originally from offsets 24..blocksize-csum_size in root).

After the loop, the final `de->rec_len` is set to:
```
data2 + (blocksize - csum_size) - (char*)de
```
where `de` is now in `data2`. The intent is "extend the last entry to fill up to the end of the data area in this block." Since the new block doesn't have the dot/dotdot prefix, the data area in `data2` runs from offset 0 to offset `blocksize - csum_size` (the whole block minus tail). So this is correct!

OK so that arithmetic is intentional: the new block is a regular directory data block, full size, with entries starting at offset 0.

### Real concern: missing validation of the last entry

The loop validates entries except the last. If the final `de` has corrupt fields (e.g., huge `rec_len`), the loop exits because `de2 >= top` (i.e., walking past it goes off the end), but `de` itself was never validated. Then `de->rec_len` is overwritten — that's actually fine, it's *replacing* the bogus rec_len. So overwriting is benign.

But there's no check that `data2 + (blocksize-csum_size) - (char*)de` is sensible. Since `de < top <= data2 + blocksize - csum_size`, and the first `de=data2`, all `de` values produced by the loop are entries whose `de2 = ext4_next_entry(de) < top` was validated by `ext4_check_dir_entry` (which ensures rlen%4==0 and reasonable size). So `de` is always at a 4-aligned offset within `[data2, top)`, making the final `rec_len` valid.

For the very *first* entry: it might fail validation of its own rec_len, but since the loop walks first... if `de = data2` has bad rec_len causing `de2 >= top`, the loop body never runs and `de` is never validated. Then `rec_len` is overwritten with a correct value. Benign.

### Conclusion

I don't see an obvious memory-corruption vulnerability in this snippet. The bounds are managed via `len` and `top`, and `ext4_check_dir_entry` is called on intermediate entries. The final entry's bogus rec_len is overwritten by design. The `ext4_rec_len_to_disk` call gets a length in `[1, blocksize]` since `de` is within `[data2, top)` and target end is `data2 + blocksize - csum_size >= top`. The only way this could BUG() is if the result is not divisible by 4. That requires `(char*)de` to be misaligned mod 4 — but `data2` is block-aligned and every previous `de2` survived `ext4_check_dir_entry`'s `rlen % 4 == 0` check, so `de` is always 4-aligned. Even the unchecked initial `de = data2` is aligned.

Note historically there were fixes here (e.g., commit b5776e7524af "ext4: avoid trim error on fs with small groups", and commit "ext4: validate the rec_len" etc.). The check `if ((char *) de >= (((char *) root) + blocksize))` was added precisely to prevent the original bug where `de` could be past the end. This snippet contains that fix.

Given the supplied context shows the validation guard for `de >= root + blocksize`, the inner loop calls `ext4_check_dir_entry`, and `ext4_rec_len_to_disk` arithmetic is safe given those invariants — **I do not see an exploitable vulnerability in this version**.

### Residual uncertainty

- The first iteration's `de` (= `data2`) is technically unchecked before `ext4_next_entry(de)` reads its `rec_len`. But the read is in-bounds, and the resulting `de2` is bounded against `top`. After validation by `ext4_check_dir_entry`, behavior is safe.
- An on-disk-corrupted `..` entry could supply `rec_len` such that the computed source `de` is at byte `blocksize - 1` (just inside). Then `len = 1 + something` mod alignment... actually `len = root + (blocksize - csum_size) - de`, and there's no check that `len` is 4-aligned or that `de` is 4-aligned. If `..`'s `rec_len` is e.g. 25 (not 4-aligned), then `de` is at offset 25, `len = blocksize - csum_size - 25` (not 4-aligned). Then in the `ext4_rec_len_to_disk` call at the end, `data2 + (blocksize - csum_size) - de_final` — `de_final` is `de_initial = data2` if loop didn't execute, giving `blocksize - csum_size` which is 4-aligned (csum_size = sizeof(ext4_dir_entry_tail) = 12, 4-aligned). So OK.
  - But if loop did execute, intermediate `de` positions advance by validated `rlen`s (4-aligned), so `de` offset = 0 + multiples of 4 = 4-aligned. OK.
  - So the misaligned starting offset of the *source* doesn't propagate to misaligned *destination* `de` values, because destination starts at offset 0.

Actually wait — `ext4_rec_len_from_disk` of `..`'s rec_len: the `..` entry's own rec_len was not validated either. It could be 25. But `ext4_check_dir_entry` was presumably called before `make_indexed_dir` is invoked (in `ext4_add_entry`'s callers). And there's also the explicit guard `if ((char *)de >= root + blocksize)`. But not a `% 4` check.

If `..`'s rec_len = 25, then `de_in_root` = at offset 25. `len = blocksize - csum_size - 25`. Now in the loop walking `data2`, the first `de` = `data2` (offset 0, aligned). `ext4_check_dir_entry` reads its rlen. The bytes at `data2[0..]` were copied from `root[25..]`, so what's there depends on what was in the source block. Could be anything corrupt.

Still, `ext4_check_dir_entry` validates rlen%4==0, so any propagation downstream is 4-aligned.

### Verdict

**Not vulnerable** based on the supplied code. The combined checks `de >= root + blocksize` guard, `ext4_check_dir_entry` per-entry validation, the bounded copy length `len`, and the `top` boundary in the walk loop together prevent OOB and BUG() conditions. The seeming "unchecked last entry" is intentional — its `rec_len` is unconditionally rewritten to a correct value, and `de`'s position is itself derived from validated 4-aligned offsets (or is `data2` itself, also aligned).

No realistic attacker-controlled path produces OOB write, OOB read with security impact, or kernel BUG from this function alone.

fs/ext4/namei.c make_indexed_dir, full body

No attacker-controlled path identified that bypasses the supplied guards. The '..' entry boundary check (de >= root + blocksize) prevents OOB before the copy. The copied length is bounded by len = root + blocksize - csum_size - de. The validation loop calls ext4_check_dir_entry on each entry except the unconditionally-rewritten final one, and rec_len%4 enforcement keeps subsequent de positions 4-aligned. Any corrupt final rec_len is overwritten, not dereferenced.

Would require a corrupted on-disk directory image where '..' rec_len lets de stay within the block but the new-block walk produces a de such that data2 + (blocksize - csum_size) - de is misaligned or >blocksize, triggering BUG() in ext4_rec_len_to_disk. This is not reachable: data2 is block-aligned, de starts at data2 and only advances by validated 4-aligned rlens, and the target end is data2 + (blocksize - csum_size) which is 4-aligned and >= de.

None observed in this function in isolation.

1) The explicit guard 'if ((char *) de >= ((char *) root) + blocksize)' rejects a corrupt '..' rec_len that would push de beyond the source block. 2) len is computed as root + (blocksize - csum_size) - de, bounding both the memcpy and the walk via top = data2 + len. 3) The walk uses ext4_check_dir_entry on every intermediate entry, which enforces rlen >= EXT4_DIR_REC_LEN(1), rlen%4==0, rlen >= EXT4_DIR_REC_LEN(name_len), and next_offset <= size, so any de that becomes 'current' is at a 4-aligned offset within [data2, top). 4) The unvalidated last de (or initial de=data2 if the loop body never executes) is simply overwritten with a freshly computed rec_len; its prior contents are not used for arithmetic beyond rec_len rewriting. 5) ext4_rec_len_to_disk receives a length in (0, blocksize] that is 4-aligned (data2 4-aligned, csum_size is sizeof(ext4_dir_entry_tail)=12, de offsets are multiples of 4), so the BUG() preconditions (len > blocksize, len & 3) cannot trigger.

After the explicit guard, de lies strictly inside the source block: de < root + blocksize.

len <= blocksize - csum_size and is the size of the memcpy into data2.

All de positions consumed in the walk are 4-aligned offsets of data2 and lie in [data2, top).

The final de->rec_len assignment uses an argument equal to (blocksize - csum_size) - (de - data2), which is in (0, blocksize] and 4-aligned.

ext4_check_dir_entry validates rec_len%4==0, lower bound, and next_offset<=size for every intermediate entry.

Traced de from initial value data2 (offset 0, 4-aligned) through ext4_next_entry advancements; each step is gated by ext4_check_dir_entry which enforces rlen%4==0 and next_offset<=len, keeping de offsets multiples of 4 inside [0, len). The final rec_len overwrite uses (blocksize - csum_size) - de_offset; since blocksize is a power of two and csum_size is 0 or 12 (4-aligned), the result is 4-aligned and within (0, blocksize]. Thus ext4_rec_len_to_disk's BUG conditions are unreachable.

The very first de=data2 is read by ext4_next_entry before being validated; this read is in-bounds. If callers/contexts upstream of make_indexed_dir do not validate the rest of the source block before this conversion, on-disk corruption could produce odd intermediate values, but they are still caught by ext4_check_dir_entry inside the loop, with EFSCORRUPTED returned. No memory-safety impact identified from the supplied code alone.

do_sys_openat2 -> path_openat -> ext4_create -> ext4_add_nondir -> ext4_add_entry -> make_indexed_dir