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: github.com/chamilo/chamilo-lms
Language: PHP
File: main/inc/lib/formvalidator/Element/HtmlEditor.php
Function: getValue

Call path: HtmlEditor::toHtml (main/inc/lib/formvalidator/Element/HtmlEditor.php) → HtmlEditor::getValue (main/inc/lib/formvalidator/Element/HtmlEditor.php) → HtmlEditor::getFrozenHtml (main/inc/lib/formvalidator/Element/HtmlEditor.php) → Security::remove_XSS (main/inc/lib/security.lib.php) → RemoveOnAttributes::filter (src/CoreBundle/Component/HTMLPurifier/Filter/RemoveOnAttributes.php)

### Primary Function

```php
public function getFrozenHtml()
    {
        return Security::remove_XSS($this->getValue());
    }
```

### Cross-File Context

[HtmlEditor — class — main/inc/lib/formvalidator/Element/HtmlEditor.php]
class HtmlEditor extends HTML_QuickForm_textarea

[getFrozenHtml — method — main/inc/lib/formvalidator/Element/HtmlEditor.php:82-86]
public function getFrozenHtml() { return Security::remove_XSS($this->getValue()); }

[Security::remove_XSS — sink — main/inc/lib/security.lib.php]
```php
function remove_XSS($html)
```

Control flow: User submits HTML content via form using HtmlEditor -> content stored in $this->_value -> when rendered, getValue() returns raw value -> getFrozenHtml() calls Security::remove_XSS() -> XSS filter may not catch all on* vectors

Data flow: User-controlled HTML input flows from $_POST/$_REQUEST through HtmlEditor constructor into $this->_value, then through getValue() (which had no filtering pre-fix) to rendering methods getFrozenHtml()/buildEditor() where Security::remove_XSS() is applied.

Verdict: Vulnerable
Vulnerable region:
[main/inc/lib/formvalidator/Element/HtmlEditor.php]
```php
/**
     * Returns the html area content in HTML.
     *
     * @return string
     */
```
CWE: CWE-79
Severity: HIGH

Root cause: The HtmlEditor class's getValue() method (added in fix) returned the raw $this->_value without removing dangerous HTML event handler attributes (on* attributes like onclick, onerror, onload, etc.). When this value was rendered via getFrozenHtml() -> Security::remove_XSS(), only some XSS vectors were blocked but on* attributes could bypass the protection. An attacker could craft HTML content with malicious on* attributes that would be stored and rendered without proper sanitization.

Exploitability: An authenticated user with access to HTML editor fields can inject malicious HTML containing on* event handler attributes (onclick, onerror, onload, etc.) that bypass existing XSS filters and execute arbitrary JavaScript when content is viewed by other users.

Attack preconditions:
- Attacker must have access to a form/area that uses HtmlEditor component
- Attacker must be able to submit HTML content with malicious on* attributes
- Content must be stored and later rendered without additional sanitization

Likely impact: An authenticated user with access to HTML editor fields can inject malicious HTML containing on* event handler attributes (onclick, onerror, onload, etc.) that bypass existing XSS filters and execute arbitrary JavaScript when content is viewed by other users.

HtmlEditor::toHtml HtmlEditor::getValue HtmlEditor::getFrozenHtml Security::remove_XSS RemoveOnAttributes::filter

### What the code does (as shown)

```php
public function getFrozenHtml()
{
    return Security::remove_XSS($this->getValue());
}
```

`getFrozenHtml()` takes the current editor value (`$this->getValue()`) and runs it through `Security::remove_XSS()`, then returns the sanitized HTML. The call path provided shows that ultimately this is filtered by `RemoveOnAttributes::filter`, i.e., at least one stage explicitly strips `on*=` event handler attributes.

This method is used for a *frozen* (read-only) rendering of the element (`HtmlEditor::toHtml → getValue → getFrozenHtml …`), which is exactly the case where you want to display previously-entered HTML safely.

### Attacker influence / data flow

- **Source**: `$this->getValue()` is the HTML editor content; this is typically user-controlled input.
- **Transformation / guard**: `Security::remove_XSS($html)` is invoked before returning.
- **Sink**: The returned string is meant to be embedded into HTML output (frozen element display). The method’s purpose is to ensure that output is XSS-safe.

### Is there a real vulnerability here?

**Not by itself, based on the supplied code.** The primary security question is whether `Security::remove_XSS()` is a robust HTML sanitizer. The snippet provided does not show its implementation; it only shows that it exists and that a filter called `RemoveOnAttributes::filter` is involved somewhere in the chain.

From the evidence we *do* have:

- There **is an explicit sanitization step** before returning HTML to be displayed.
- The downstream filter name strongly suggests removal of inline event handlers (`onclick`, etc.), which is a common XSS vector.

From the evidence we *don’t* have:

- We cannot validate that `remove_XSS()` covers other dangerous constructs (e.g., `javascript:` URLs in `href/src`, `style`/CSS-based vectors, `<svg>`/MathML edge cases, malformed tag parsing quirks, DOM clobbering, etc.).
- We cannot confirm whether `remove_XSS()` is context-aware (HTML body vs. attribute vs. JS context). Here it appears intended for HTML body context, which is appropriate.

### Conclusion / verdict

- **Verdict**: **No concrete vulnerability is demonstrated in the shown function.** It follows a correct pattern (sanitize-before-render) for a frozen HTML display.
- **Residual risk / uncertainty**: The security of this depends entirely on `Security::remove_XSS()`’s correctness and coverage. If `remove_XSS()` is a homegrown/insufficient sanitizer or misconfigured HTMLPurifier, XSS might still be possible—but that cannot be concluded from the provided excerpt alone.

### What to check next to confirm safety

To upgrade this from “likely safe” to “confirmed safe,” you would need to inspect:

1. The full implementation of `Security::remove_XSS($html)` (does it use HTMLPurifier properly? what config/allowed tags/attributes/schemes?).
2. Whether it strips/normalizes dangerous URI schemes (`javascript:`, `data:`), dangerous tags (`script`, `iframe`, `object`), and CSS expressions.
3. Any bypass history in Chamilo for this sanitizer, and unit tests / regression tests for known XSS vectors.

As it stands, `getFrozenHtml()` is *doing the right thing* and is not itself an obvious vulnerability.

Not demonstrated from provided code. The method applies an XSS sanitization routine before returning HTML for frozen display.

Attacker can influence HtmlEditor element value (e.g., submit HTML content).

Returned value is rendered into an HTML context by the caller.

getFrozenHtml() performs output sanitization by passing the (potentially user-controlled) editor value through Security::remove_XSS() before returning it for rendering. This is a standard guard against reflected/stored XSS in read-only/frozen HTML output.

All HTML returned by getFrozenHtml() is processed by Security::remove_XSS().

Call path indicates an additional purification stage (RemoveOnAttributes::filter) that strips inline event-handler attributes (on*).

Direct code evidence shows return Security::remove_XSS($this->getValue());. Provided call path confirms traversal into RemoveOnAttributes::filter during sanitization.

The actual strength of protection depends on the correctness and configuration of Security::remove_XSS() and its underlying filters; the implementation is not shown here, so sanitizer bypasses (e.g., via URI schemes, SVG/MathML quirks, CSS vectors, malformed markup) cannot be ruled out from this snippet alone.

If the returned value is later embedded into a non-HTML-body context (attribute, JS string, CSS), remove_XSS may be insufficient; no such usage is shown in the provided context.

HtmlEditor::toHtml -> HtmlEditor::getValue -> HtmlEditor::getFrozenHtml -> Security::remove_XSS -> RemoveOnAttributes::filter


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/stable/linux
Language: C
File: kernel/sched/fair.c
Function: reweight_eevdf

Call path: reweight_eevdf (kernel/sched/fair.c) → entity_eligible (kernel/sched/fair.c) → pick_eevdf (kernel/sched/fair.c) → update_entity_lag (kernel/sched/fair.c) → calc_delta_fair (kernel/sched/fair.c) → avg_vruntime (kernel/sched/fair.c)

### Primary Function

```c
static void reweight_eevdf(struct sched_entity *se, u64 avruntime,
			   unsigned long weight)
{
	unsigned long old_weight = se->load.weight;
	s64 vlag, vslice;

	/*
	 * VRUNTIME
	 * ========
	 *
	 * COROLLARY #1: The virtual runtime of the entity needs to be
	 * adjusted if re-weight at !0-lag point.
	 *
	 * Proof: For contradiction assume this is not true, so we can
	 * re-weight without changing vruntime at !0-lag point.
	 *
	 *             Weight	VRuntime   Avg-VRuntime
	 *     before    w          v            V
	 *      after    w'         v'           V'
	 *
	 * Since lag needs to be preserved through re-weight:
	 *
	 *	lag = (V - v)*w = (V'- v')*w', where v = v'
	 *	==>	V' = (V - v)*w/w' + v		(1)
	 *
	 * Let W be the total weight of the entities before reweight,
	 * since V' is the new weighted average of entities:
	 *
	 *	V' = (WV + w'v - wv) / (W + w' - w)	(2)
	 *
	 * by using (1) & (2) we obtain:
	 *
	 *	(WV + w'v - wv) / (W + w' - w) = (V - v)*w/w' + v
	 *	==> (WV-Wv+Wv+w'v-wv)/(W+w'-w) = (V - v)*w/w' + v
	 *	==> (WV - Wv)/(W + w' - w) + v = (V - v)*w/w' + v
	 *	==>	(V - v)*W/(W + w' - w) = (V - v)*w/w' (3)
	 *
	 * Since we are doing at !0-lag point which means V != v, we
	 * can simplify (3):
	 *
	 *	==>	W / (W + w' - w) = w / w'
	 *	==>	Ww' = Ww + ww' - ww
	 *	==>	W * (w' - w) = w * (w' - w)
	 *	==>	W = w	(re-weight indicates w' != w)
	 *
	 * So the cfs_rq contains only one entity, hence vruntime of
	 * the entity @v should always equal to the cfs_rq's weighted
	 * average vruntime @V, which means we will always re-weight
	 * at 0-lag point, thus breach assumption. Proof completed.
	 *
	 *
	 * COROLLARY #2: Re-weight does NOT affect weighted average
	 * vruntime of all the entities.
	 *
	 * Proof: According to corollary #1, Eq. (1) should be:
	 *
	 *	(V - v)*w = (V' - v')*w'
	 *	==>	    v' = V' - (V - v)*w/w'		(4)
	 *
	 * According to the weighted average formula, we have:
	 *
	 *	V' = (WV - wv + w'v') / (W - w + w')
	 *	   = (WV - wv + w'(V' - (V - v)w/w')) / (W - w + w')
	 *	   = (WV - wv + w'V' - Vw + wv) / (W - w + w')
	 *	   = (WV + w'V' - Vw) / (W - w + w')
	 *
	 *	==>	V'*(W - w + w') = WV + w'V' - Vw
	 *	==>	V' * (W - w) = (W - w) * V	(5)
	 *
	 * If the entity is the only one in the cfs_rq, then reweight
	 * always occurs at 0-lag point, so V won't change. Or else
	 * there are other entities, hence W != w, then Eq. (5) turns
	 * into V' = V. So V won't change in either case, proof done.
	 *
	 *
	 * So according to corollary #1 & #2, the effect of re-weight
	 * on vruntime should be:
	 *
	 *	v' = V' - (V - v) * w / w'		(4)
	 *	   = V  - (V - v) * w / w'
	 *	   = V  - vl * w / w'
	 *	   = V  - vl'
	 */
	if (avruntime != se->vruntime) {
		vlag = entity_lag(avruntime, se);
		vlag = div_s64(vlag * old_weight, weight);
		se->vruntime = avruntime - vlag;
	}

	/*
	 * DEADLINE
	 * ========
	 *
	 * When the weight changes, the virtual time slope changes and
	 * we should adjust the relative virtual deadline accordingly.
	 *
	 *	d' = v' + (d - v)*w/w'
	 *	   = V' - (V - v)*w/w' + (d - v)*w/w'
	 *	   = V  - (V - v)*w/w' + (d - v)*w/w'
	 *	   = V  + (d - V)*w/w'
	 */
	vslice = (s64)(se->deadline - avruntime);
	vslice = div_s64(vslice * old_weight, weight);
	se->deadline = avruntime + vslice;
}
```

### Cross-File Context

[entity_lag — function — kernel/sched/fair.c:699]
```c
static s64 entity_lag(u64 avruntime, struct sched_entity *se)
{
	s64 vlag, limit;

	vlag = avruntime - se->vruntime;
	limit = calc_delta_fair(max_t(u64, 2*se->slice, TICK_NSEC), se);

	return clamp(vlag, -limit, limit);
}
```

[update_entity_lag — helper — kernel/sched/fair.c:709]
```c
static void update_entity_lag(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	SCHED_WARN_ON(!se->on_rq);

	se->vlag = entity_lag(avg_vruntime(cfs_rq), se);
}
```

[calc_delta_fair — helper — kernel/sched/fair.c:296]
```c
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD))
		delta = __calc_delta(delta, NICE_0_LOAD, &se->load);

	return delta;
}
```

[avg_vruntime — helper — kernel/sched/fair.c:665]
```c
u64 avg_vruntime(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	s64 avg = cfs_rq->avg_vruntime;
	long load = cfs_rq->avg_load;

	if (curr && curr->on_rq) {
		unsigned long weight = scale_load_down(curr->load.weight);

		avg += entity_key(cfs_rq, curr) * weight;
		load += weight;
	}

	if (load) {
		/* sign flips effective floor / ceil */
		if (avg < 0)
			avg -= (load - 1);
		avg = div_s64(avg, load);
	}

	return cfs_rq->min_vruntime + avg;
}
```

[entity_eligible — callee — kernel/sched/fair.c:749]
```c
int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	return vruntime_eligible(cfs_rq, se->vruntime);
}
```

[pick_eevdf — callee — kernel/sched/fair.c:884]
```c
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq)
{
	struct rb_node *node = cfs_rq->tasks_timeline.rb_root.rb_node;
	struct sched_entity *se = __pick_first_entity(cfs_rq);
	struct sched_entity *curr = cfs_rq->curr;
	struct sched_entity *best = NULL;

	if (cfs_rq->nr_running == 1)
		return curr && curr->on_rq ? curr : se;

	if (curr && (!curr->on_rq || !entity_eligible(cfs_rq, curr)))
		curr = NULL;

	if (sched_feat(RUN_TO_PARITY) && curr && curr->vlag == curr->deadline)
		return curr;

	if (se && entity_eligible(cfs_rq, se)) {
		best = se;
		goto found;
	}

	while (node) {
		struct rb_node *left = node->rb_left;

		if (left && vruntime_eligible(cfs_rq,
					__node_2_se(left)->min_vruntime)) {
			node = left;
			continue;
		}

		se = __node_2_se(node);

		if (entity_eligible(cfs_rq, se)) {
			best = se;
			break;
		}

		node = node->rb_right;
	}
found:
	if (!best || (curr && entity_before(curr, best)))
		best = curr;

	return best;
}
```

[sched_entity — struct — include/linux/sched.h:536]
```c
struct sched_entity {
	/* For load-balancing: */
	struct load_weight		load;
	struct rb_node			run_node;
	u64				deadline;
	u64				min_vruntime;

	struct list_head		group_node;
	unsigned int			on_rq;

	u64				exec_start;
	u64			sum_exec_runtime;
	u64			prev_sum_exec_runtime;
	u64			vruntime;
	s64			vlag;
	u64			slice;

	u64			nr_migrations;

#ifdef CONFIG_FAIR_GROUP_SCHED
	int			depth;
	struct sched_entity	*parent;
	/* rq on which this entity is (to be) queued: */
	struct cfs_rq		*cfs_rq;
	/* rq "owned" by this entity/group: */
	struct cfs_rq		*my_q;
	/* cached value of my_q->h_nr_running */
	unsigned long		-runnable_weight;
#endif

#ifdef CONFIG_SMP
	/*
	 * Per entity load average tracking.
	 *
	 * Put into separate cache line so it does not
	 * collide with read-mostly values above.
	 */
	struct sched_avg	avg;
#endif
};
```

[clamp — macro — linux/kernel.h]
clamp → #define clamp(val, lo, hi) min_t(typeof(val), max_t(typeof(val), val, lo), hi)  (linux/kernel.h)

[TICK_NSEC — constant — linux/jiffies.h]
TICK_NSEC → (NSEC_PER_SEC / HZ)  (linux/jiffies.h)

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 reweight_eevdf function has two sequential branches: (1) VRUNTIME adjustment block that executes only when avruntime != se->vruntime, computing entity_lag, scaling by weight ratio via div_s64, and updating se->vruntime; (2) DEADLINE adjustment block that unconditionally computes vslice from se->deadline and avruntime, scales by weight ratio via div_s64, and updates se->deadline. There are no error paths, cleanup paths, or early returns—both sections execute linearly when their conditions are met.

Data flow: Input: se (sched_entity pointer), avruntime (u64 weighted average vruntime), weight (unsigned long new weight). Intermediate: old_weight extracted from se->load.weight. In the VRUNTIME branch, entity_lag(avruntime, se) returns a clamped lag value via entity_lag() which subtracts se->vruntime from avruntime and clamps to [-limit, limit] where limit is calc_delta_fair(max_t(u64, 2*se->slice, TICK_NSEC), se). The clamped vlag is then multiplied by old_weight and divided by weight using div_s64, producing a scaled lag. se->vruntime is set to avruntime - scaled_vlag. In the DEADLINE branch, vslice is computed as (se->deadline - avruntime) cast to s64, then scaled by old_weight/weight via div_s64. se->deadline is set to avruntime + scaled_vslice. All output writes modify fields within se.

Verdict: Not Vulnerable

Reviewed region:
[kernel/sched/fair.c]
```c
static void reweight_eevdf(struct sched_entity *se, u64 avruntime,
			   unsigned long weight)
{
	unsigned long old_weight = se->load.weight;
	s64 vlag, vslice;

	/*
	 * VRUNTIME
	 * ========
	 *
	 * COROLLARY #1: The virtual runtime of the entity needs to be
	 * adjusted if re-weight at !0-lag point.
	 *
	 * Proof: For contradiction assume this is not true, so we can
	 * re-weight without changing vruntime at !0-lag point.
	 *
	 *             Weight	VRuntime   Avg-VRuntime
	 *     before    w          v            V
	 *      after    w'         v'           V'
	 *
	 * Since lag needs to be preserved through re-weight:
	 *
	 *	lag = (V - v)*w = (V'- v')*w', where v = v'
	 *	==>	V' = (V - v)*w/w' + v		(1)
	 *
	 * Let W be the total weight of the entities before reweight,
	 * since V' is the new weighted average of entities:
	 *
	 *	V' = (WV + w'v - wv) / (W + w' - w)	(2)
	 *
	 * by using (1) & (2) we obtain:
	 *
	 *	(WV + w'v - wv) / (W + w' - w) = (V - v)*w/w' + v
	 *	==> (WV-Wv+Wv+w'v-wv)/(W+w'-w) = (V - v)*w/w' + v
	 *	==> (WV - Wv)/(W + w' - w) + v = (V - v)*w/w' + v
	 *	==>	(V - v)*W/(W + w' - w) = (V - v)*w/w' (3)
	 *
	 * Since we are doing at !0-lag point which means V != v, we
	 * can simplify (3):
	 *
	 *	==>	W / (W + w' - w) = w / w'
	 *	==>	Ww' = Ww + ww' - ww
	 *	==>	W * (w' - w) = w * (w' - w)
	 *	==>	W = w	(re-weight indicates w' != w)
	 *
	 * So the cfs_rq contains only one entity, hence vruntime of
	 * the entity @v should always equal to the cfs_rq's weighted
	 * average vruntime @V, which means we will always re-weight
	 * at 0-lag point, thus breach assumption. Proof completed.
	 *
	 *
	 * COROLLARY #2: Re-weight does NOT affect weighted average
	 * vruntime of all the entities.
	 *
	 * Proof: According to corollary #1, Eq. (1) should be:
	 *
	 *	(V - v)*w = (V' - v')*w'
	 *	==>	    v' = V' - (V - v)*w/w'		(4)
	 *
	 * According to the weighted average formula, we have:
	 *
	 *	V' = (WV - wv + w'v') / (W - w + w')
	 *	   = (WV - wv + w'(V' - (V - v)w/w')) / (W - w + w')
	 *	   = (WV - wv + w'V' - Vw + wv) / (W - w + w')
	 *	   = (WV + w'V' - Vw) / (W - w + w')
	 *
	 *	==>	V'*(W - w + w') = WV + w'V' - Vw
	 *	==>	V' * (W - w) = (W - w) * V	(5)
	 *
	 * If the entity is the only one in the cfs_rq, then reweight
	 * always occurs at 0-lag point, so V won't change. Or else
	 * there are other entities, hence W != w, then Eq. (5) turns
	 * into V' = V. So V won't change in either case, proof done.
	 *
	 *
	 * So according to corollary #1 & #2, the effect of re-weight
	 * on vruntime should be:
	 *
	 *	v' = V' - (V - v) * w / w'		(4)
	 *	   = V  - (V - v) * w / w'
	 *	   = V  - vl * w / w'
	 *	   = V  - vl'
	 */
	if (avruntime != se->vruntime) {
		vlag = entity_lag(avruntime, se);
		vlag = div_s64(vlag * old_weight, weight);
		se->vruntime = avruntime - vlag;
	}

	/*
	 * DEADLINE
	 * ========
	 *
	 * When the weight changes, the virtual time slope changes and
	 * we should adjust the relative virtual deadline accordingly.
	 *
	 *	d' = v' + (d - v)*w/w'
	 *	   = V' - (V - v)*w/w' + (d - v)*w/w'
	 *	   = V  - (V - v)*w/w' + (d - v)*w/w'
	 *	   = V  + (d - V)*w/w'
	 */
	vslice = (s64)(se->deadline - avruntime);
	vslice = div_s64(vslice * old_weight, weight);
	se->deadline = avruntime + vslice;
}
```

Why it is not vulnerable: The critical operation vlag * old_weight in the VRUNTIME branch cannot overflow because vlag is clamped by entity_lag() to [-limit, limit] where limit = calc_delta_fair(max_t(u64, 2*se->slice, TICK_NSEC), se). Since entity_lag() uses clamp() with a bounded limit derived from the scheduling quantum (slice and TICK_NSEC), the intermediate product vlag * old_weight remains well within s64 range. The div_s64() function safely handles division including zero-divisor edge cases. Similarly, vslice * old_weight in the DEADLINE branch uses a time difference that is bounded by reasonable scheduling deadlines, and div_s64() provides safe signed division semantics.

Security invariants:
- vlag must be bounded before multiplication by old_weight; enforced by entity_lag() which returns clamp(vlag, -limit, limit) where limit = calc_delta_fair(max_t(u64, 2*se->slice, TICK_NSEC), se)
- Division by weight must not cause division-by-zero or undefined behavior; enforced by div_s64() which checks for zero divisor and returns a safe fallback value
- The signed arithmetic in vlag = div_s64(vlag * old_weight, weight) must not overflow; enforced by the clamping invariant limiting vlag magnitude and old_weight being bounded by the kernel weight scale
- The signed cast (s64)(se->deadline - avruntime) must represent a valid time difference; enforced by deadline and avruntime both being u64 nanosecond timestamps from the same scheduling domain

Invariant verification:
- vlag clamping before scaling multiplication: holds=true. Evidence: entity_lag() computes vlag = avruntime - se->vruntime, then limit = calc_delta_fair(max_t(u64, 2*se->slice, TICK_NSEC), se), then returns clamp(vlag, -limit, limit). This clamped result is passed to reweight_eevdf which multiplies it by old_weight in div_s64(vlag * old_weight, weight).
- Safe signed division with zero-divisor protection: holds=true. Evidence: Both vlag and vslice scaling use div_s64() for division by weight. div_s64() is a kernel utility that checks for zero divisors and returns a defined fallback, preventing division-by-zero or undefined behavior.
- Bounded intermediate product magnitude: holds=true. Evidence: Since vlag is clamped to [-limit, limit] and old_weight is an unsigned long bounded by MAX_WEIGHT (1024 in kernel scheduler), the product vlag * old_weight fits within s64 (±2^63). The limit itself is derived from TICK_NSEC and se->slice, both bounded time values.
- vslice computation uses valid time representation: holds=true. Evidence: vslice = (s64)(se->deadline - avruntime) where both se->deadline and avruntime are u64 nanosecond timestamps. The subtraction produces a reasonable time difference that, when scaled by weight ratio, fits within s64 range for normal scheduling parameters.

Residual risk notes: The vslice calculation in the DEADLINE branch does not apply explicit clamping like the vlag path. While vslice represents a bounded scheduling deadline difference in practice, an unusually large deadline value combined with a large old_weight could theoretically approach s64 limits. However, se->deadline is set by the scheduler using avruntime + vslice patterns that keep values in reasonable ranges, and div_s64 provides overflow-safe division semantics. The weight parameter is not validated for zero at the call site, but div_s64 handles this defensively.

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

### Verdict: **Real vulnerability is plausible (division-by-zero / crash), contingent on caller guarantees not shown**

`reweight_eevdf()` performs multiple signed divisions by the `weight` argument without any local validation that `weight != 0`:

```c
vlag = div_s64(vlag * old_weight, weight);
...
vslice = div_s64(vslice * old_weight, weight);
```

In the Linux kernel, a divide-by-zero in this context will typically trigger a kernel exception/oops/panic, i.e., a **denial of service**.

---

## What is security-relevant here

### Direct issue: unchecked divisor
- `weight` is an `unsigned long` input to `reweight_eevdf`.
- It is used as the divisor in `div_s64(...)` twice.
- There is **no guard** like `if (!weight) ...` or a clamp to a minimum value.

If `weight == 0`, both divisions are undefined/illegal and will fault.

### Data/control flow notes grounded in shown code
- `old_weight` comes from `se->load.weight` (also unsigned long). That being zero would be odd, but it doesn’t itself cause the fault; it’s the *divisor* that matters.
- `entity_lag()` clamps `vlag` to `[-limit, limit]`, which helps prevent overflow in `vlag`, but does **nothing** to ensure the divisor is non-zero.
- `avg_vruntime()` has a `if (load)` check before dividing by `load`; that function is robust against its divisor being zero. `reweight_eevdf()` lacks an analogous check.

---

## Exploitability / attacker influence

Whether this is *actually reachable* depends on whether all call sites ensure `weight` is always non-zero.

In the scheduler, weights generally come from:
- nice values / `sched_prio_to_weight[]`,
- cgroup CPU controller weights,
- or internal load calculations.

Those are **normally** constrained to be positive, but that guarantee is not present in the provided snippet. If any path allows:
- `weight` to be set to 0 due to a bug, misconfiguration edge case, integer underflow, or unexpected conversion, or
- a caller passing `weight` derived from an untrusted/loosely validated interface (e.g., cgroup configuration) that can become 0,

then a local unprivileged user could potentially trigger a kernel crash by manipulating scheduling/cgroup parameters and hitting the reweight path.

Because the provided context does not include the callers of `reweight_eevdf()` nor the constraints on `weight`, we can’t prove reachability from user input *from this excerpt alone*—but the absence of a defensive check means the function is **fragile** and will crash if the invariant is ever violated.

---

## Impact
- **Kernel crash / denial of service** if `weight == 0` reaches this function.
- No direct evidence of memory corruption or privilege escalation from the shown code; the primary risk is a fault due to divide-by-zero.

---

## Classification
- **CWE-369: Divide by Zero**
- Kernel DoS (crash) upon triggering the bad state.

---

## What would make it “not a vulnerability”
If (and only if) the kernel globally enforces an invariant that scheduler entity weights are **always ≥ 1** and every call site passes such a guaranteed-nonzero `weight`, then the code is safe in practice. That invariant is common in the scheduler, but it’s not demonstrated in the provided call path/context.

---

## Hardening recommendation (minimal)
Add a local guard to enforce the invariant at the boundary:

```c
if (WARN_ON_ONCE(!weight))
    return; /* or weight = 1; */
```

or clamp:

```c
weight = max(weight, 1UL);
```

Which response is appropriate depends on scheduler policy, but either prevents a fatal divide-by-zero if an upstream invariant is ever broken.

medium (kernel DoS if invariant violated), otherwise none

div_s64(vlag * old_weight, weight)
div_s64(vslice * old_weight, weight)
no local check for weight==0

The function divides by the caller-supplied 'weight' argument without validating it is non-zero. If weight==0 ever reaches this function, kernel division by zero occurs (undefined behavior / trap).

Contingent. In the Linux scheduler, entity weights are typically guaranteed to be positive (>0) by upstream code (nice-to-weight tables, cgroup constraints, load initialization). That invariant is not shown in the provided context/call path, so reachability from attacker-controlled inputs cannot be established here. If any path can set or pass weight==0, this becomes a straightforward kernel crash trigger.

An execution path exists that calls reweight_eevdf(se, avruntime, weight) with weight==0

Attacker can influence that path (e.g., via scheduler/cgroup configuration or a separate bug that corrupts/zeros weights) and cause the reweight operation to occur

If reachable with weight==0: kernel oops/panic due to divide-by-zero -> local denial of service. No evidence in the shown code of memory corruption or privilege escalation.

From the provided snippets alone, we cannot show that 'weight' can be zero. Scheduler weights are generally constrained to be non-zero by design; if that global invariant holds for all callers, the division is safe and no vulnerability exists.

Invariant required for safety: weight != 0 for all calls to reweight_eevdf

Likely (but not demonstrated here): se->load.weight and derived weights are always >= 1 in CFS

Not verifiable from the supplied code/context. Callers of reweight_eevdf and weight derivation/clamping logic are not included.

Even if current callers enforce weight>0, lack of a local defensive check makes this code fragile to future changes or unexpected states (e.g., corruption/bugs elsewhere).

Hardening would be to WARN/guard/clamp weight at the function boundary to prevent catastrophic failure if invariants break.

reweight_eevdf (kernel/sched/fair.c)
entity_eligible (kernel/sched/fair.c)
pick_eevdf (kernel/sched/fair.c)
update_entity_lag (kernel/sched/fair.c)
calc_delta_fair (kernel/sched/fair.c)
avg_vruntime (kernel/sched/fair.c)

MEDIUM (KERNEL DOS IF INVARIANT VIOLATED), OTHERWISE NONE


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: https://github.com/torvalds/linux
Language: C
File: drivers/clk/sunxi-ng/ccu_common.c
Function: sunxi_ccu_probe

Call path: devm_sunxi_ccu_probe (drivers/clk/sunxi-ng/ccu_common.c) → of_sunxi_ccu_probe (drivers/clk/sunxi-ng/ccu_common.c) → sunxi_ccu_probe (drivers/clk/sunxi-ng/ccu_common.c) → hw_to_ccu_common (drivers/clk/sunxi-ng/ccu_common.h)

### Primary Function

```c
static int sunxi_ccu_probe(struct sunxi_ccu *ccu, struct device *dev,
			   struct device_node *node, void __iomem *reg,
			   const struct sunxi_ccu_desc *desc)
{
	struct ccu_reset *reset;
	int i, ret;

	ccu->desc = desc;

	spin_lock_init(&ccu->lock);

	for (i = 0; i < desc->num_ccu_clks; i++) {
		struct ccu_common *cclk = desc->ccu_clks[i];

		if (!cclk)
			continue;

		cclk->base = reg;
		cclk->lock = &ccu->lock;
	}

	for (i = 0; i < desc->hw_clks->num ; i++) {
		struct clk_hw *hw = desc->hw_clks->hws[i];
		struct ccu_common *common = hw_to_ccu_common(hw);
		const char *name;

		if (!hw)
			continue;

		name = hw->init->name;
		if (dev)
			ret = clk_hw_register(dev, hw);
		else
			ret = of_clk_hw_register(node, hw);
		if (ret) {
			pr_err("Couldn't register clock %d - %s\n", i, name);
			goto err_clk_unreg;
		}

		if (common->max_rate)
			clk_hw_set_rate_range(hw, common->min_rate,
					      common->max_rate);
		else
			WARN(common->min_rate,
			     "No max_rate, ignoring min_rate of clock %d - %s\n",
			     i, name);
	}

	ret = of_clk_add_hw_provider(node, of_clk_hw_onecell_get,
				     desc->hw_clks);
	if (ret)
		goto err_clk_unreg;

	reset = &ccu->reset;
	reset->rcdev.of_node = node;
	reset->rcdev.ops = &ccu_reset_ops;
	reset->rcdev.owner = dev ? dev->driver->owner : THIS_MODULE;
	reset->rcdev.nr_resets = desc->num_resets;
	reset->base = reg;
	reset->lock = &ccu->lock;
	reset->reset_map = desc->resets;

	ret = reset_controller_register(&reset->rcdev);
	if (ret)
		goto err_del_provider;

	return 0;

err_del_provider:
	of_clk_del_provider(node);
err_clk_unreg:
	while (--i >= 0) {
		struct clk_hw *hw = desc->hw_clks->hws[i];

		if (!hw)
			continue;
		clk_hw_unregister(hw);
	}
	return ret;
}
```

### Cross-File Context

[hw_to_ccu_common — helper — drivers/clk/sunxi-ng/ccu_common.h:42]
hw_to_ccu_common → static inline struct ccu_common *hw_to_ccu_common(struct clk_hw *hw) { return container_of(hw, struct ccu_common, hw); }  (drivers/clk/sunxi-ng/ccu_common.h:42)

[ccu_common — struct — drivers/clk/sunxi-ng/ccu_common.h:28]
```c
struct ccu_common {
	void __iomem	*base;
	u16		reg;
	u16		lock_reg;
	u32		prediv;

	unsigned long	min_rate;
	unsigned long	max_rate;

	unsigned long	features;
	spinlock_t	*lock;
	struct clk_hw	hw;
};
```

[sunxi_ccu_desc — struct — drivers/clk/sunxi-ng/ccu_common.h:47]
```c
struct sunxi_ccu_desc {
	struct ccu_common		**ccu_clks;
	unsigned long			num_ccu_clks;

	struct clk_hw_onecell_data	*hw_clks;

	struct ccu_reset_map		*resets;
	unsigned long			num_resets;
};
```

[clk_hw_set_rate_range — function — include/linux/clk-provider.h]
```c
void clk_hw_set_rate_range(struct clk_hw *hw, unsigned long min_rate, unsigned long max_rate)
```

Control flow: sunxi_ccu_probe iterates over hw_clks->hws[] to register clocks. In the vulnerable version, it calls hw_to_ccu_common on each hw entry and immediately dereferences the result. The fix separates clock registration from rate range setting, using a second loop over ccu_clks[] which are guaranteed to be ccu_common structs.

Data flow: The attacker-controlled data flows from DeviceTree descriptor (desc->hw_clks->hws[]) through hw_to_ccu_common() which performs an unchecked type cast via container_of. The resulting pointer is dereferenced to access min_rate and max_rate fields without validation.

Verdict: Vulnerable
Vulnerable region:
[drivers/clk/sunxi-ng/ccu_common.c]
```c
for (i = 0; i < desc->hw_clks->num ; i++) {
		struct clk_hw *hw = desc->hw_clks->hws[i];
		struct ccu_common *common = hw_to_ccu_common(hw);
		const char *name;

		if (!hw)
			continue;

		name = hw->init->name;
		if (dev)
			ret = clk_hw_register(dev, hw);
		else
			ret = of_clk_hw_register(node, hw);
		if (ret) {
			pr_err("Couldn't register clock %d - %s\n", i, name);
			goto err_clk_unreg;
		}

		if (common->max_rate)
			clk_hw_set_rate_range(hw, common->min_rate,
					      common->max_rate);
		else
			WARN(common->min_rate,
			     "No max_rate, ignoring min_rate of clock %d - %s\n",
			     i, name);
	}

	ret = of_clk_add_hw_provider(node, of_clk_hw_onecell_get,
				     desc->hw_clks);
	if (ret)
		goto err_clk_unreg;
```
CWE: CWE-476
Severity: MEDIUM

Root cause: The sunxi_ccu_probe function incorrectly assumes all entries in desc->hw_clks->hws[] are ccu_common structs by using hw_to_ccu_common(hw) which is a container_of cast. When hw points to a non-ccu_common clk_hw, the cast produces an invalid pointer, and subsequent access to common->max_rate or common->min_rate causes an invalid pointer dereference.

Exploitability: The vulnerability is triggered during device probe when a sunxi CCU clock device has a hw_clks->hws[] array containing entries that are not ccu_common structs. The buggy code causes immediate dereference of an invalid pointer constructed via container_of on the wrong type.

Attack preconditions:
- Target must have a DeviceTree node with a sunxi CCU clock controller
- The ccu_clks array in the device descriptor must contain at least one non-ccu_common clk_hw entry
- The clock driver probe function (sunxi_ccu_probe or its wrappers) must be invoked

Likely impact: The vulnerability is triggered during device probe when a sunxi CCU clock device has a hw_clks->hws[] array containing entries that are not ccu_common structs. The buggy code causes immediate dereference of an invalid pointer constructed via container_of on the wrong type.

### Verdict: **Real vulnerability (NULL pointer dereference) if `desc->hw_clks->hws[i]` can be NULL**

This probe routine has a concrete NULL-deref bug in the `hw_clks` registration loop. It is likely reachable on error paths or malformed/incorrect descriptor data (and in the kernel, “malformed” often means a driver bug or unexpected build-time table content rather than untrusted user input). Still, it’s a real crashable condition.

---

## What the code does (relevant parts)

In the second loop:

```c
for (i = 0; i < desc->hw_clks->num ; i++) {
    struct clk_hw *hw = desc->hw_clks->hws[i];
    struct ccu_common *common = hw_to_ccu_common(hw);
    const char *name;

    if (!hw)
        continue;

    name = hw->init->name;
    ...
```

### Problem: `hw_to_ccu_common(hw)` is called **before** checking `if (!hw)`

`hw_to_ccu_common()` is:

```c
return container_of(hw, struct ccu_common, hw);
```

`container_of(NULL, ...)` computes an address based on `(char *)NULL - offsetof(...)`, producing an invalid non-NULL pointer. That pointer is then used later:

```c
if (common->max_rate)
    clk_hw_set_rate_range(hw, common->min_rate, common->max_rate);
else
    WARN(common->min_rate, ...);
```

So if `hw` is NULL for any slot, the `if (!hw) continue;` comes too late; the function will already have derived `common` from NULL, and will dereference it.

---

## Exploitability / impact

- **Impact:** kernel crash (oops/panic) during probe of the CCU driver if any `desc->hw_clks->hws[i]` is NULL.
- **Classification:** NULL pointer dereference (CWE-476). In kernel terms: local DoS / stability issue.
- **Attacker influence:** This is not directly user-controlled in typical deployments because `desc` and the `hws[]` array are static driver data, not supplied by userspace. However:
  - It can be triggered by **driver integration mistakes**, incomplete tables, or conditional compilation leaving holes.
  - Potentially triggerable by a **malicious or buggy kernel module** providing an invalid `sunxi_ccu_desc` to `sunxi_ccu_probe()` (less relevant in mainline where this is internal plumbing).
  - It’s not a privilege escalation by itself, but it is a **reliable crash** if reachable.

---

## Why existing guards don’t help

The code clearly *intends* to tolerate NULL entries:

```c
if (!hw)
    continue;
```

But because `common` is computed before this check, the guard is ineffective.

---

## Fix (minimal)

Move the `hw_to_ccu_common()` call after the NULL check:

```c
struct clk_hw *hw = desc->hw_clks->hws[i];
struct ccu_common *common;
const char *name;

if (!hw)
    continue;

common = hw_to_ccu_common(hw);
name = hw->init->name;
...
```

Optionally also guard `hw->init` if there’s any chance it can be NULL (not shown here).

---

## Residual notes

- The first loop correctly checks `if (!cclk) continue;` before dereferencing.
- The error-unregister loop is fine for NULL `hw` entries (it checks before unregistering).

**Bottom line:** as written, `sunxi_ccu_probe()` can dereference an invalid pointer derived from a NULL `hw`, causing a kernel crash.

drivers/clk/sunxi-ng/ccu_common.c:sunxi_ccu_probe (entire function, with focus on hw registration loop and hw_to_ccu_common usage)

Not exploitable as a real NULL dereference in the shown code path; the computed pointer from container_of(NULL, ...) is not dereferenced because control flow immediately continues on NULL hw.

Would require desc->hw_clks->hws[i] == NULL and an additional dereference of the derived 'common' pointer before the NULL check. In the provided code, no such dereference occurs.

None from the alleged issue; no crash is triggered solely by a NULL hw entry in the shown loop due to the early continue before any use of 'common' or 'name'.

Although hw_to_ccu_common(hw) is called before checking hw for NULL, the result is not dereferenced or otherwise used prior to the subsequent `if (!hw) continue;`. Therefore, a NULL hw entry does not cause an invalid memory access in this function as shown.

If `hw` is NULL, the loop iteration executes `continue` before any dereference of `hw`, `hw->init`, or `common`.

All dereferences of `common` (common->max_rate/min_rate) occur only after the `if (!hw) continue;` guard, implying `hw` is non-NULL at that point.

Data/control-flow trace: `hw` loaded; `common` computed but unused; `if (!hw) continue;` prevents reaching `name = hw->init->name;` and `if (common->max_rate) ...` when hw is NULL. Thus no dereference on NULL-derived pointer occurs in the NULL case.

If future edits introduce any use of `common` (or `name`) before the `if (!hw)` guard, the pattern could become a real NULL-derived invalid dereference. Current code avoids it.

Separate potential robustness issues not established by provided context: `desc`, `desc->hw_clks`, `desc->hw_clks->hws`, and `hw->init` are assumed valid; if any can be NULL, other NULL dereferences could exist, but such conditions are not evidenced/controlled in the supplied snippet/context.


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/bus.c
Function: typec_altmode_attention

Call path: tcpm_handle_vdm_request (drivers/usb/typec/tcpm/tcpm.c) → typec_altmode_attention (drivers/usb/typec/bus.c) → partner->adev.ops->attention (drivers/usb/typec/bus.c)

### Primary Function

```c
int typec_altmode_attention(struct typec_altmode *adev, u32 vdo)
{
	struct altmode *partner = to_altmode(adev)->partner;
	struct typec_altmode *pdev;

	if (!partner)
		return -ENODEV;

	pdev = &partner->adev;

	if (pdev->ops && pdev->ops->attention)
		pdev->ops->attention(pdev, vdo);

	return 0;
}
```

### Cross-File Context

[altmode — struct — drivers/usb/typec/bus.h]
```c
struct altmode {
	unsigned int			id;
	struct typec_altmode		adev;
	struct typec_mux		*mux;

	enum typec_port_data		roles;

	struct attribute		*attrs[5];
	char				group_name[8];
	struct attribute_group		group;
	const struct attribute_group	*groups[2];

	struct altmode			*partner;
	struct altmode			*plug[2];
};
```

[to_altmode — macro — drivers/usb/typec/bus.h]
to_altmode → #define to_altmode(d) container_of(d, struct altmode, adev)  (drivers/usb/typec/bus.h)

[typec_altmode — struct — include/linux/usb/typec_altmode.h]
```c
struct typec_altmode {
	struct device			dev;
	u16				svid;
	int				mode;
	u32				vdo;
	unsigned int			active:1;

	char				*desc;
	const struct typec_altmode_ops	*ops;
};
```

[typec_altmode_ops — struct — include/linux/usb/typec_altmode.h]
```c
struct typec_altmode_ops {
	int (*enter)(struct typec_altmode *altmode, u32 *vdo);
	int (*exit)(struct typec_altmode *altmode);
	void (*attention)(struct typec_altmode *altmode, u32 vdo);
	int (*vdm)(struct typec_altmode *altmode, const u32 hdr,
		   const u32 *vdo, int cnt);
	int (*notify)(struct typec_altmode *altmode, unsigned long conf,
		      void *data);
	int (*activate)(struct typec_altmode *altmode, int activate);
};
```

Control flow: The function receives adev and vdo, extracts the partner altmode via container_of(adev)->partner, checks if partner is NULL and returns -ENODEV if so, otherwise derives pdev from partner->adev, checks that pdev->ops and pdev->ops->attention are non-NULL before invoking the callback, and returns 0 on success. The only error path is the early return with -ENODEV when partner is NULL.

Data flow: Input adev is transformed via to_altmode(adev) to get the enclosing struct altmode, then partner is extracted from its partner field. After the NULL check passes, pdev is derived as &partner->adev. The vdo parameter is passed through unchanged to the attention callback if it exists. The function returns -ENODEV on NULL partner or 0 on success.

Verdict: Not Vulnerable

Reviewed region:
[drivers/usb/typec/bus.c]
```c
int typec_altmode_attention(struct typec_altmode *adev, u32 vdo)
{
	struct altmode *partner = to_altmode(adev)->partner;
	struct typec_altmode *pdev;

	if (!partner)
		return -ENODEV;

	pdev = &partner->adev;

	if (pdev->ops && pdev->ops->attention)
		pdev->ops->attention(pdev, vdo);

	return 0;
}
```

Why it is not vulnerable: The code explicitly checks `if (!partner) return -ENODEV;` before any dereference of the partner pointer. This NULL guard ensures that `pdev = &partner->adev;` is only reached when partner is valid. Additionally, the callback invocation is guarded by `if (pdev->ops && pdev->ops->attention)` preventing dereference of a NULL function pointer.

Security invariants:
- partner must be non-NULL before dereferencing partner->adev — enforced by `if (!partner) return -ENODEV;`
- pdev->ops must be non-NULL before accessing pdev->ops->attention — enforced by `if (pdev->ops && pdev->ops->attention)`
- pdev->ops->attention must be non-NULL before invoking the callback — enforced by the same compound guard `if (pdev->ops && pdev->ops->attention)`

Invariant verification:
- NULL pointer check on partner before dereference: holds=true. Evidence: The statement `if (!partner) return -ENODEV;` immediately follows `struct altmode *partner = to_altmode(adev)->partner;` and precedes `pdev = &partner->adev;`, ensuring partner is validated before access.
- Callback function pointer validation before invocation: holds=true. Evidence: The condition `if (pdev->ops && pdev->ops->attention)` guards the call to `pdev->ops->attention(pdev, vdo);`, verifying both ops and the attention function pointer are non-NULL.

Residual risk notes: The validity of the adev input parameter and the correctness of container_of(adev) are assumed — a corrupted or freed adev pointer would result in undefined behavior before the partner NULL check. Additionally, the attention callback is invoked without holding any visible lock in this function; the caller (tcpm_handle_vdm_request) is responsible for appropriate synchronization. These are contextual concerns rather than vulnerabilities in this function itself.

### What the function does (grounded in code)

`typec_altmode_attention()` is a small dispatcher:

1. Takes an `adev` pointer (a `struct typec_altmode *`) and a 32-bit `vdo`.
2. Converts `adev` to its embedding `struct altmode` via `to_altmode(adev)` (`container_of`).
3. Reads `partner = to_altmode(adev)->partner`.
4. If `partner` is NULL, returns `-ENODEV`.
5. Otherwise sets `pdev = &partner->adev`.
6. If `pdev->ops && pdev->ops->attention`, it calls `pdev->ops->attention(pdev, vdo)`.
7. Returns 0.

So the only “action” is an indirect function call through a function pointer stored in the partner altmode’s ops table.

---

### Potential vulnerability angles and what the code actually shows

#### 1) NULL pointer dereference
- `partner` is checked for NULL before use.
- `pdev->ops` and `pdev->ops->attention` are checked before call.
- So within this function, there’s no obvious NULL deref.

**Residual risk**: `adev` itself is not checked for NULL. If a caller passed NULL, `to_altmode(adev)` would crash. But in the given call path (`tcpm_handle_vdm_request → typec_altmode_attention`), `adev` should be an internal kernel object, not an untrusted pointer. Nothing here indicates user-controlled invocation with arbitrary `adev`.

#### 2) Out-of-bounds / container_of misuse
`to_altmode(d)` is `container_of(d, struct altmode, adev)`. This is only safe if `d` genuinely points to the `adev` field inside a valid `struct altmode`. If an attacker could supply an arbitrary `struct typec_altmode *` pointer, `container_of` would compute an attacker-chosen `struct altmode *` and then dereference `->partner`, potentially leading to memory disclosure or control-flow hijack.

**But**: Based on the context, `typec_altmode_attention()` is an internal kernel API used within the Type‑C/altmode subsystem. There is no evidence in the supplied code that `adev` can come from userspace or from an untrusted address. In normal kernel design, these objects are allocated/registered by kernel drivers, not by userspace.

So: *theoretical* hazard if misused, but **not a demonstrated vulnerability** from the shown code path.

#### 3) Function pointer call / control-flow hijack
The call `pdev->ops->attention(pdev, vdo)` is an indirect call. If an attacker could corrupt `pdev->ops` or `pdev->ops->attention`, this becomes a control-flow integrity problem.

From the snippet:
- `ops` lives inside `struct typec_altmode` (`const struct typec_altmode_ops *ops;`).
- The code does not lock or refcount `partner` or `pdev` here.

**Key point**: nothing in this snippet shows any attacker-controlled memory write that could corrupt `ops`, nor does it show a use-after-free. The indirect call itself is not a vulnerability unless there is a separate memory corruption bug or lifetime race elsewhere.

#### 4) Use-after-free / concurrency lifetime issues
This is the most plausible class to worry about: `partner` is read from `to_altmode(adev)->partner` with no visible locking or reference management. If another thread could concurrently free/unregister `partner` or modify `->partner`, you could get:
- UAF when doing `pdev = &partner->adev`, or
- calling `pdev->ops->attention` after free.

However, **the supplied context does not include**:
- how `partner` is set/cleared,
- what locks protect it (if any),
- whether `struct device` lifetimes/refcounts are held during VDM handling,
- whether tcpm serializes these callbacks.

So a UAF cannot be concluded from this function alone. It’s a *possible concern*, but not provable as a “real vulnerability” with the evidence shown.

---

### Attacker influence / exploitability (based on call path)
- The `vdo` comes from a VDM attention message, i.e., potentially from a connected Type‑C partner device. That is attacker-influenceable at the hardware/protocol level.
- But `vdo` is only passed as data into a driver callback; this function itself does not interpret it or use it for memory indexing, allocation, or length calculations.
- Any security impact would depend on the implementation of `pdev->ops->attention` in the specific altmode driver, not on this dispatcher.

---

### Verdict
**No real vulnerability is demonstrated in the provided function and context.** The function performs basic NULL checks and only forwards an attention event to a registered partner altmode callback.

**What remains uncertain / worth checking outside this snippet**:
- Whether `partner` / `pdev` lifetime is properly protected (locking/refcounting) across this call chain to rule out UAF races.
- Whether any altmode attention handlers mishandle attacker-controlled `vdo` (that would be in the altmode driver, not here).

If you can provide the surrounding code managing `altmode->partner` assignment/removal and the locking used during VDM handling, I can assess the UAF/race possibility concretely.

Not exploitable based on shown code. The function only dispatches a Type-C Alt Mode attention event to a registered kernel callback after NULL checks. It does not perform memory-unsafe operations on attacker-controlled data (vdo) itself, and does not expose a direct user-controlled pointer path for adev/partner/pdev.

Attacker would need a separate memory corruption or lifetime bug elsewhere (e.g., UAF/race on partner or corruption of ops function pointers) to turn the indirect call into control-flow hijack.

Or a misuse of this internal API where a non-altmode-embedded pointer is passed as adev (container_of misuse), which is not evidenced by the supplied call path/context.

None attributable to this function in isolation. Any security impact would depend on vulnerabilities in the callee implementation (pdev->ops->attention) or on external lifetime/race bugs not shown here.

Within this function, all dereferences after obtaining partner are guarded: partner is checked for NULL, and the callback invocation is guarded by checking pdev->ops and pdev->ops->attention. The input vdo is only forwarded as a value; it is not used for indexing, allocation, or pointer arithmetic here. The function is an internal dispatcher in the kernel Type-C subsystem, and the snippet provides no evidence that adev is attacker-controlled or that partner lifetime is unsafe in this context.

partner must be non-NULL before pdev is derived and used (enforced).

pdev->ops must be non-NULL and pdev->ops->attention must be non-NULL before indirect call (enforced).

adev is expected to be a valid pointer to the 'adev' field embedded in a live 'struct altmode' (assumed by design; not verified in this function).

Verified in code: explicit `if (!partner) return -ENODEV;` prevents NULL deref of partner.

Verified in code: `if (pdev->ops && pdev->ops->attention)` prevents NULL function pointer call.

Not verifiable from supplied context: lifetime/locking/refcounting for partner and validity of `adev` for container_of.

Potential UAF/race concern: partner is read and used without visible locking/refcounting in this snippet; whether this is safe depends on external synchronization in tcpm/typec core not provided.

Potential container_of misuse: if any caller could pass an arbitrary/invalid `struct typec_altmode *`, `to_altmode()` could compute an invalid base pointer; the provided call path suggests internal use, but full call graph/validation is not shown.

The vdo originates from a connected device (hardware attacker). Any parsing/validation issues would be in the attention handler implementations, not here.


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: github.com/the-tcpdump-group/tcpdump
Language: C
File: print-pim.c
Function: pimv2_print

Call path: main (tcpdump.c) → pim_print (print-pim.c) → pimv2_print (print-pim.c)

### Primary Function

```c
static void
pimv2_print(netdissect_options *ndo,
            register const u_char *bp, register u_int len, const u_char *bp2)
{
	register const u_char *ep;
	register const struct pim *pim = (const struct pim *)bp;
	int advance;
	enum checksum_status cksum_status;

	ep = (const u_char *)ndo->ndo_snapend;
	if (bp >= ep)
		return;
	if (ep > bp + len)
		ep = bp + len;
	ND_TCHECK(pim->pim_rsv);
	pimv2_addr_len = pim->pim_rsv;
	if (pimv2_addr_len != 0)
		ND_PRINT((ndo, ", RFC2117-encoding"));

	ND_PRINT((ndo, ", cksum 0x%04x ", EXTRACT_16BITS(&pim->pim_cksum)));
	if (EXTRACT_16BITS(&pim->pim_cksum) == 0) {
		ND_PRINT((ndo, "(unverified)"));
	} else {
		if (PIM_TYPE(pim->pim_typever) == PIMV2_TYPE_REGISTER) {
			/*
			 * The checksum only covers the packet header,
			 * not the encapsulated packet.
			 */
			cksum_status = pimv2_check_checksum(ndo, bp, bp2, 8);
			if (cksum_status == INCORRECT) {
				/*
				 * To quote RFC 4601, "For interoperability
				 * reasons, a message carrying a checksum
				 * calculated over the entire PIM Register
				 * message should also be accepted."
				 */
				cksum_status = pimv2_check_checksum(ndo, bp, bp2, len);
			}
		} else {
			/*
			 * The checksum covers the entire packet.
			 */
			cksum_status = pimv2_check_checksum(ndo, bp, bp2, len);
		}
		switch (cksum_status) {

		case CORRECT:
			ND_PRINT((ndo, "(correct)"));
			break;

		case INCORRECT:
			ND_PRINT((ndo, "(incorrect)"));
			break;

		case UNVERIFIED:
			ND_PRINT((ndo, "(unverified)"));
			break;
		}
	}

	switch (PIM_TYPE(pim->pim_typever)) {
	case PIMV2_TYPE_HELLO:
	    {
		uint16_t otype, olen;
		bp += 4;
		while (bp < ep) {
			ND_TCHECK2(bp[0], 4);
			otype = EXTRACT_16BITS(&bp[0]);
			olen = EXTRACT_16BITS(&bp[2]);
			ND_TCHECK2(bp[0], 4 + olen);
			ND_PRINT((ndo, "\n\t  %s Option (%u), length %u, Value: ",
			          tok2str(pimv2_hello_option_values, "Unknown", otype),
			          otype,
			          olen));
			bp += 4;

			switch (otype) {
			case PIMV2_HELLO_OPTION_HOLDTIME:
				if (olen != 2) {
					ND_PRINT((ndo, "ERROR: Option Length != 2 Bytes (%u)", olen));
				} else {
					unsigned_relts_print(ndo, EXTRACT_16BITS(bp));
				}
				break;

			case PIMV2_HELLO_OPTION_LANPRUNEDELAY:
				if (olen != 4) {
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
				} else {
					char t_bit;
					uint16_t lan_delay, override_interval;
					lan_delay = EXTRACT_16BITS(bp);
					override_interval = EXTRACT_16BITS(bp+2);
					t_bit = (lan_delay & 0x8000)? 1 : 0;
					lan_delay &= ~0x8000;
					ND_PRINT((ndo, "\n\t    T-bit=%d, LAN delay %dms, Override interval %dms",
					t_bit, lan_delay, override_interval));
				}
				break;

			case PIMV2_HELLO_OPTION_DR_PRIORITY_OLD:
			case PIMV2_HELLO_OPTION_DR_PRIORITY:
				switch (olen) {
				case 0:
					ND_PRINT((ndo, "Bi-Directional Capability (Old)"));
					break;
				case 4:
					ND_PRINT((ndo, "%u", EXTRACT_32BITS(bp)));
					break;
				default:
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
					break;
				}
				break;

			case PIMV2_HELLO_OPTION_GENID:
				if (olen != 4) {
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
				} else {
					ND_PRINT((ndo, "0x%08x", EXTRACT_32BITS(bp)));
				}
				break;

			case PIMV2_HELLO_OPTION_REFRESH_CAP:
				if (olen != 4) {
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
				} else {
					ND_PRINT((ndo, "v%d", *bp));
					if (*(bp+1) != 0) {
						ND_PRINT((ndo, ", interval "));
						unsigned_relts_print(ndo, *(bp+1));
					}
					if (EXTRACT_16BITS(bp+2) != 0) {
						ND_PRINT((ndo, " ?0x%04x?", EXTRACT_16BITS(bp+2)));
					}
				}
				break;

			case  PIMV2_HELLO_OPTION_BIDIR_CAP:
				break;

			case PIMV2_HELLO_OPTION_ADDRESS_LIST_OLD:
			case PIMV2_HELLO_OPTION_ADDRESS_LIST:
				if (ndo->ndo_vflag > 1) {
					const u_char *ptr = bp;
					while (ptr < (bp+olen)) {
						ND_PRINT((ndo, "\n\t    "));
						advance = pimv2_addr_print(ndo, ptr, pimv2_unicast, 0);
						if (advance < 0) {
							ND_PRINT((ndo, "..."));
							break;
						}
						ptr += advance;
					}
				}
				break;
			default:
				if (ndo->ndo_vflag <= 1)
					print_unknown_data(ndo, bp, "\n\t    ", olen);
				break;
			}
			/* do we want to see an additionally hexdump ? */
			if (ndo->ndo_vflag> 1)
				print_unknown_data(ndo, bp, "\n\t    ", olen);
			bp += olen;
		}
		break;
	    }

	case PIMV2_TYPE_REGISTER:
	{
		const struct ip *ip;

		ND_TCHECK2(*(bp + 4), PIMV2_REGISTER_FLAG_LEN);

		ND_PRINT((ndo, ", Flags [ %s ]\n\t",
		          tok2str(pimv2_register_flag_values,
		          "none",
		          EXTRACT_32BITS(bp+4))));

		bp += 8; len -= 8;
		/* encapsulated multicast packet */
		ip = (const struct ip *)bp;
		switch (IP_V(ip)) {
                case 0: /* Null header */
			ND_PRINT((ndo, "IP-Null-header %s > %s",
			          ipaddr_string(ndo, &ip->ip_src),
			          ipaddr_string(ndo, &ip->ip_dst)));
			break;

		case 4:	/* IPv4 */
			ip_print(ndo, bp, len);
			break;

		case 6:	/* IPv6 */
			ip6_print(ndo, bp, len);
			break;

		default:
			ND_PRINT((ndo, "IP ver %d", IP_V(ip)));
			break;
		}
		break;
	}

	case PIMV2_TYPE_REGISTER_STOP:
		bp += 4; len -= 4;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " group="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " source="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		break;

	case PIMV2_TYPE_JOIN_PRUNE:
	case PIMV2_TYPE_GRAFT:
	case PIMV2_TYPE_GRAFT_ACK:

        /*
         * 0                   1                   2                   3
         *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |PIM Ver| Type  | Addr length   |           Checksum            |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |             Unicast-Upstream Neighbor Address                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |  Reserved     | Num groups    |          Holdtime             |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |            Encoded-Multicast Group Address-1                  |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |   Number of Joined  Sources   |   Number of Pruned Sources    |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Joined Source Address-1                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                             .                                 |
         *  |                             .                                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Joined Source Address-n                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Pruned Source Address-1                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                             .                                 |
         *  |                             .                                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Pruned Source Address-n                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                           .                                   |
         *  |                           .                                   |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                Encoded-Multicast Group Address-n              |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         */

	    {
		uint8_t ngroup;
		uint16_t holdtime;
		uint16_t njoin;
		uint16_t nprune;
		int i, j;

		bp += 4; len -= 4;
		if (PIM_TYPE(pim->pim_typever) != 7) {	/*not for Graft-ACK*/
			if (bp >= ep)
				break;
			ND_PRINT((ndo, ", upstream-neighbor: "));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
				ND_PRINT((ndo, "..."));
				break;
			}
			bp += advance; len -= advance;
		}
		if (bp + 4 > ep)
			break;
		ngroup = bp[1];
		holdtime = EXTRACT_16BITS(&bp[2]);
		ND_PRINT((ndo, "\n\t  %u group(s)", ngroup));
		if (PIM_TYPE(pim->pim_typever) != 7) {	/*not for Graft-ACK*/
			ND_PRINT((ndo, ", holdtime: "));
			if (holdtime == 0xffff)
				ND_PRINT((ndo, "infinite"));
			else
				unsigned_relts_print(ndo, holdtime);
		}
		bp += 4; len -= 4;
		for (i = 0; i < ngroup; i++) {
			if (bp >= ep)
				goto jp_done;
			ND_PRINT((ndo, "\n\t    group #%u: ", i+1));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
				ND_PRINT((ndo, "...)"));
				goto jp_done;
			}
			bp += advance; len -= advance;
			if (bp + 4 > ep) {
				ND_PRINT((ndo, "...)"));
				goto jp_done;
			}
			njoin = EXTRACT_16BITS(&bp[0]);
			nprune = EXTRACT_16BITS(&bp[2]);
			ND_PRINT((ndo, ", joined sources: %u, pruned sources: %u", njoin, nprune));
			bp += 4; len -= 4;
			for (j = 0; j < njoin; j++) {
				ND_PRINT((ndo, "\n\t      joined source #%u: ", j+1));
				if ((advance = pimv2_addr_print(ndo, bp, pimv2_source, 0)) < 0) {
					ND_PRINT((ndo, "...)"));
					goto jp_done;
				}
				bp += advance; len -= advance;
			}
			for (j = 0; j < nprune; j++) {
				ND_PRINT((ndo, "\n\t      pruned source #%u: ", j+1));
				if ((advance = pimv2_addr_print(ndo, bp, pimv2_source, 0)) < 0) {
					ND_PRINT((ndo, "...)"));
					goto jp_done;
				}
				bp += advance; len -= advance;
			}
		}
	jp_done:
		break;
	    }

	case PIMV2_TYPE_BOOTSTRAP:
	{
		int i, j, frpcnt;
		bp += 4;

		/* Fragment Tag, Hash Mask len, and BSR-priority */
		if (bp + sizeof(uint16_t) >= ep) break;
		ND_PRINT((ndo, " tag=%x", EXTRACT_16BITS(bp)));
		bp += sizeof(uint16_t);
		if (bp >= ep) break;
		ND_PRINT((ndo, " hashmlen=%d", bp[0]));
		if (bp + 1 >= ep) break;
		ND_PRINT((ndo, " BSRprio=%d", bp[1]));
		bp += 2;

		/* Encoded-Unicast-BSR-Address */
		if (bp >= ep) break;
		ND_PRINT((ndo, " BSR="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;

		for (i = 0; bp < ep; i++) {
			/* Encoded-Group Address */
			ND_PRINT((ndo, " (group%d: ", i));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0))
			    < 0) {
				ND_PRINT((ndo, "...)"));
				goto bs_done;
			}
			bp += advance;

			/* RP-Count, Frag RP-Cnt, and rsvd */
			if (bp >= ep) {
				ND_PRINT((ndo, "...)"));
				goto bs_done;
			}
			ND_PRINT((ndo, " RPcnt=%d", bp[0]));
			if (bp + 1 >= ep) {
				ND_PRINT((ndo, "...)"));
				goto bs_done;
			}
			ND_PRINT((ndo, " FRPcnt=%d", frpcnt = bp[1]));
			bp += 4;

			for (j = 0; j < frpcnt && bp < ep; j++) {
				/* each RP info */
				ND_PRINT((ndo, " RP%d=", j));
				if ((advance = pimv2_addr_print(ndo, bp,
								pimv2_unicast,
								0)) < 0) {
					ND_PRINT((ndo, "...)"));
					goto bs_done;
				}
				bp += advance;

				if (bp + 1 >= ep) {
					ND_PRINT((ndo, "...)"));
					goto bs_done;
				}
				ND_PRINT((ndo, ",holdtime="));
				unsigned_relts_print(ndo, EXTRACT_16BITS(bp));
				if (bp + 2 >= ep) {
					ND_PRINT((ndo, "...)"));
					goto bs_done;
				}
				ND_PRINT((ndo, ",prio=%d", bp[2]));
				bp += 4;
			}
			ND_PRINT((ndo, ")"));
		}
	   bs_done:
		break;
	}
	case PIMV2_TYPE_ASSERT:
		bp += 4; len -= 4;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " group="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " src="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		if (bp + 8 > ep)
			break;
		if (bp[0] & 0x80)
			ND_PRINT((ndo, " RPT"));
		ND_PRINT((ndo, " pref=%u", EXTRACT_32BITS(&bp[0]) & 0x7fffffff));
		ND_PRINT((ndo, " metric=%u", EXTRACT_32BITS(&bp[4])));
		break;

	case PIMV2_TYPE_CANDIDATE_RP:
	{
		int i, pfxcnt;
		bp += 4;

		/* Prefix-Cnt, Priority, and Holdtime */
		if (bp >= ep) break;
		ND_PRINT((ndo, " prefix-cnt=%d", bp[0]));
		pfxcnt = bp[0];
		if (bp + 1 >= ep) break;
		ND_PRINT((ndo, " prio=%d", bp[1]));
		if (bp + 3 >= ep) break;
		ND_PRINT((ndo, " holdtime="));
		unsigned_relts_print(ndo, EXTRACT_16BITS(&bp[2]));
		bp += 4;

		/* Encoded-Unicast-RP-Address */
		if (bp >= ep) break;
		ND_PRINT((ndo, " RP="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;

		/* Encoded-Group Addresses */
		for (i = 0; i < pfxcnt && bp < ep; i++) {
			ND_PRINT((ndo, " Group%d=", i));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0))
			    < 0) {
				ND_PRINT((ndo, "..."));
				break;
			}
			bp += advance;
		}
		break;
	}

	case PIMV2_TYPE_PRUNE_REFRESH:
		ND_PRINT((ndo, " src="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;
		ND_PRINT((ndo, " grp="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;
		ND_PRINT((ndo, " forwarder="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;
		ND_TCHECK2(bp[0], 2);
		ND_PRINT((ndo, " TUNR "));
		unsigned_relts_print(ndo, EXTRACT_16BITS(bp));
		break;

	 default:
		ND_PRINT((ndo, " [type %d]", PIM_TYPE(pim->pim_typever)));
		break;
	}

	return;

trunc:
	ND_PRINT((ndo, "[|pim]"));
}
```

### Cross-File Context

[PIMV2_HELLO_OPTION_HOLDTIME — constant — print-pim.c:88]
PIMV2_HELLO_OPTION_HOLDTIME → 1  (print-pim.c:88)

[PIMV2_HELLO_OPTION_GENID — constant — print-pim.c:92]
PIMV2_HELLO_OPTION_GENID → 20  (print-pim.c:92)

[PIMV2_HELLO_OPTION_REFRESH_CAP — constant — print-pim.c:93]
PIMV2_HELLO_OPTION_REFRESH_CAP → 21  (print-pim.c:93)

[PIM_TYPE — macro — print-pim.c:135]
PIM_TYPE → #define PIM_TYPE(x) ((x) & 0x0f)  (print-pim.c:135)

[pimv2_check_checksum — function — print-pim.c:625-653]
```c
static enum checksum_status
pimv2_check_checksum(netdissect_options *ndo, const u_char *bp,
		     const u_char *bp2, u_int len)
{
	const struct ip *ip;
	u_int cksum;

	if (!ND_TTEST2(bp[0], len)) {
		return (UNVERIFIED);
	}
	ip = (const struct ip *)bp2;
	if (IP_V(ip) == 4) {
		struct cksum_vec vec[1];

		vec[0].ptr = bp;
		vec[0].len = len;
		cksum = in_cksum(vec, 1);
		return (cksum ? INCORRECT : CORRECT);
	} else if (IP_V(ip) == 6) {
		const struct ip6_hdr *ip6;

		ip6 = (const struct ip6_hdr *)bp2;
		cksum = nextproto6_cksum(ndo, ip6, bp, len, len, IPPROTO_PIM);
		return (cksum ? INCORRECT : CORRECT);
	} else {
		return (UNVERIFIED);
	}
}
```

[pimv2_addr_print — function — print-pim.c:525-607]
```c
static int
pimv2_addr_print(netdissect_options *ndo,
                 const u_char *bp, enum pimv2_addrtype at, int silent)
{
	int af;
	int len, hdrlen;

	ND_TCHECK(bp[0]);

	if (pimv2_addr_len == 0) {
		ND_TCHECK(bp[1]);
		switch (bp[0]) {
		case 1:
			af = AF_INET;
			len = sizeof(struct in_addr);
			break;
		case 2:
			af = AF_INET6;
			len = sizeof(struct in6_addr);
			break;
		default:
			return -1;
		}
		if (bp[1] != 0)
			return -1;
		hdrlen = 2;
	} else {
		switch (pimv2_addr_len) {
		case sizeof(struct in_addr):
			af = AF_INET;
			break;
		case sizeof(struct in6_addr):
			af = AF_INET6;
			break;
		default:
			return -1;
			break;
		}
		len = pimv2_addr_len;
		hdrlen = 0;
	}

	bp += hdrlen;
	switch (at) {
	case pimv2_unicast:
		ND_TCHECK2(bp[0], len);
		if (af == AF_INET) {
			if (!silent)
				ND_PRINT((ndo, "%s", ipaddr_string(ndo, bp)));
		}
		else if (af == AF_INET6) {
			if (!silent)
				ND_PRINT((ndo, "%s", ip6addr_string(ndo, bp)));
		}
		return len;

	case pimv2_group:
		ND_TCHECK2(bp[0], len);
		if (af == AF_INET) {
			if (!silent)
				ND_PRINT((ndo, "%s", ipaddr_string(ndo, bp)));
		}
		else if (af == AF_INET6) {
			if (!silent)
				ND_PRINT((ndo, "%s", ip6addr_string(ndo, bp)));
		}
		return len;

	case pimv2_source:
		ND_TCHECK2(bp[0], 2);
		if (bp[0] & 0x80)
			ND_PRINT((ndo, "(S,G) "));
		if (bp[0] & 0x40)
			ND_PRINT((ndo, "(W) "));
		if (bp[0] & 0x20)
			ND_PRINT((ndo, "(R) "));
		ND_TCHECK2(bp[0], len);
		if (af == AF_INET) {
			if (!silent)
				ND_PRINT((ndo, "%s", ipaddr_string(ndo, bp)));
		}
		else if (af == AF_INET6) {
			if (!silent)
				ND_PRINT((ndo, "%s", ip6addr_string(ndo, bp)));
		}
		return len;
	}
	return 0;
}
```

Control flow: pimv2_print enters and clamps the endpoint `ep` to the packet boundary. It checks for truncation early, then reads the PIM header fields. For checksums, it calls pimv2_check_checksum conditionally based on packet type. The main switch dispatches on PIM_TYPE(pim->pim_typever). In PIMV2_TYPE_HELLO, a while loop iterates over TLV options bounded by bp < ep, with ND_TCHECK2 at loop entry validating each option's header and value fit in the packet. Each option type case performs length validation before reading value data. The loop advances bp by the option length and continues. Other PIM message types (Register, Register-Stop, Join-Prune, Bootstrap, Assert, Candidate-RP, Prune-Refresh) parse fixed-format fields with pre-read bounds checks before each access. The function returns normally or jumps to the trunc label on insufficient data.

Data flow: Input pointer bp and length len define the PIM packet boundaries. The endpoint ep is derived from ndo_snapend and clamped to bp + len. The PIM header (rsv field, checksum, typever) is read after ND_TCHECK validation. For HELLO messages, otype and olen are extracted from the TLV header. olen is validated against expected sizes per option type (2 for HOLDTIME, 4 for LANPRUNEDELAY/GENID/REFRESH_CAP, 0 or 4 for DR_PRIORITY). The ND_TCHECK2(bp[0], 4 + olen) at loop entry ensures the entire option fits before any option-specific processing. For ADDRESS_LIST and unknown types, print_unknown_data or pimv2_addr_print consume olen bytes, protected by the earlier bounds check. For non-HELLO types, encoded addresses are parsed via pimv2_addr_print which internally validates address data availability via ND_TCHECK macros. Checksum verification reads bp and bp2 with ND_TTEST2 bounds validation.

Verdict: Not Vulnerable

Reviewed region:
[print-pim.c]
```c
static void
pimv2_print(netdissect_options *ndo,
            register const u_char *bp, register u_int len, const u_char *bp2)
{
	register const u_char *ep;
	register const struct pim *pim = (const struct pim *)bp;
	int advance;
	enum checksum_status cksum_status;

	ep = (const u_char *)ndo->ndo_snapend;
	if (bp >= ep)
		return;
	if (ep > bp + len)
		ep = bp + len;
	ND_TCHECK(pim->pim_rsv);
	pimv2_addr_len = pim->pim_rsv;
	if (pimv2_addr_len != 0)
		ND_PRINT((ndo, ", RFC2117-encoding"));

	ND_PRINT((ndo, ", cksum 0x%04x ", EXTRACT_16BITS(&pim->pim_cksum)));
	if (EXTRACT_16BITS(&pim->pim_cksum) == 0) {
		ND_PRINT((ndo, "(unverified)"));
	} else {
		if (PIM_TYPE(pim->pim_typever) == PIMV2_TYPE_REGISTER) {
			/*
			 * The checksum only covers the packet header,
			 * not the encapsulated packet.
			 */
			cksum_status = pimv2_check_checksum(ndo, bp, bp2, 8);
			if (cksum_status == INCORRECT) {
				/*
				 * To quote RFC 4601, "For interoperability
				 * reasons, a message carrying a checksum
				 * calculated over the entire PIM Register
				 * message should also be accepted."
				 */
				cksum_status = pimv2_check_checksum(ndo, bp, bp2, len);
			}
		} else {
			/*
			 * The checksum covers the entire packet.
			 */
			cksum_status = pimv2_check_checksum(ndo, bp, bp2, len);
		}
		switch (cksum_status) {

		case CORRECT:
			ND_PRINT((ndo, "(correct)"));
			break;

		case INCORRECT:
			ND_PRINT((ndo, "(incorrect)"));
			break;

		case UNVERIFIED:
			ND_PRINT((ndo, "(unverified)"));
			break;
		}
	}

	switch (PIM_TYPE(pim->pim_typever)) {
	case PIMV2_TYPE_HELLO:
	    {
		uint16_t otype, olen;
		bp += 4;
		while (bp < ep) {
			ND_TCHECK2(bp[0], 4);
			otype = EXTRACT_16BITS(&bp[0]);
			olen = EXTRACT_16BITS(&bp[2]);
			ND_TCHECK2(bp[0], 4 + olen);
			ND_PRINT((ndo, "\n\t  %s Option (%u), length %u, Value: ",
			          tok2str(pimv2_hello_option_values, "Unknown", otype),
			          otype,
			          olen));
			bp += 4;

			switch (otype) {
			case PIMV2_HELLO_OPTION_HOLDTIME:
				if (olen != 2) {
					ND_PRINT((ndo, "ERROR: Option Length != 2 Bytes (%u)", olen));
				} else {
					unsigned_relts_print(ndo, EXTRACT_16BITS(bp));
				}
				break;

			case PIMV2_HELLO_OPTION_LANPRUNEDELAY:
				if (olen != 4) {
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
				} else {
					char t_bit;
					uint16_t lan_delay, override_interval;
					lan_delay = EXTRACT_16BITS(bp);
					override_interval = EXTRACT_16BITS(bp+2);
					t_bit = (lan_delay & 0x8000)? 1 : 0;
					lan_delay &= ~0x8000;
					ND_PRINT((ndo, "\n\t    T-bit=%d, LAN delay %dms, Override interval %dms",
					t_bit, lan_delay, override_interval));
				}
				break;

			case PIMV2_HELLO_OPTION_DR_PRIORITY_OLD:
			case PIMV2_HELLO_OPTION_DR_PRIORITY:
				switch (olen) {
				case 0:
					ND_PRINT((ndo, "Bi-Directional Capability (Old)"));
					break;
				case 4:
					ND_PRINT((ndo, "%u", EXTRACT_32BITS(bp)));
					break;
				default:
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
					break;
				}
				break;

			case PIMV2_HELLO_OPTION_GENID:
				if (olen != 4) {
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
				} else {
					ND_PRINT((ndo, "0x%08x", EXTRACT_32BITS(bp)));
				}
				break;

			case PIMV2_HELLO_OPTION_REFRESH_CAP:
				if (olen != 4) {
					ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen));
				} else {
					ND_PRINT((ndo, "v%d", *bp));
					if (*(bp+1) != 0) {
						ND_PRINT((ndo, ", interval "));
						unsigned_relts_print(ndo, *(bp+1));
					}
					if (EXTRACT_16BITS(bp+2) != 0) {
						ND_PRINT((ndo, " ?0x%04x?", EXTRACT_16BITS(bp+2)));
					}
				}
				break;

			case  PIMV2_HELLO_OPTION_BIDIR_CAP:
				break;

			case PIMV2_HELLO_OPTION_ADDRESS_LIST_OLD:
			case PIMV2_HELLO_OPTION_ADDRESS_LIST:
				if (ndo->ndo_vflag > 1) {
					const u_char *ptr = bp;
					while (ptr < (bp+olen)) {
						ND_PRINT((ndo, "\n\t    "));
						advance = pimv2_addr_print(ndo, ptr, pimv2_unicast, 0);
						if (advance < 0) {
							ND_PRINT((ndo, "..."));
							break;
						}
						ptr += advance;
					}
				}
				break;
			default:
				if (ndo->ndo_vflag <= 1)
					print_unknown_data(ndo, bp, "\n\t    ", olen);
				break;
			}
			/* do we want to see an additionally hexdump ? */
			if (ndo->ndo_vflag> 1)
				print_unknown_data(ndo, bp, "\n\t    ", olen);
			bp += olen;
		}
		break;
	    }

	case PIMV2_TYPE_REGISTER:
	{
		const struct ip *ip;

		ND_TCHECK2(*(bp + 4), PIMV2_REGISTER_FLAG_LEN);

		ND_PRINT((ndo, ", Flags [ %s ]\n\t",
		          tok2str(pimv2_register_flag_values,
		          "none",
		          EXTRACT_32BITS(bp+4))));

		bp += 8; len -= 8;
		/* encapsulated multicast packet */
		ip = (const struct ip *)bp;
		switch (IP_V(ip)) {
                case 0: /* Null header */
			ND_PRINT((ndo, "IP-Null-header %s > %s",
			          ipaddr_string(ndo, &ip->ip_src),
			          ipaddr_string(ndo, &ip->ip_dst)));
			break;

		case 4:	/* IPv4 */
			ip_print(ndo, bp, len);
			break;

		case 6:	/* IPv6 */
			ip6_print(ndo, bp, len);
			break;

		default:
			ND_PRINT((ndo, "IP ver %d", IP_V(ip)));
			break;
		}
		break;
	}

	case PIMV2_TYPE_REGISTER_STOP:
		bp += 4; len -= 4;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " group="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " source="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		break;

	case PIMV2_TYPE_JOIN_PRUNE:
	case PIMV2_TYPE_GRAFT:
	case PIMV2_TYPE_GRAFT_ACK:

        /*
         * 0                   1                   2                   3
         *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |PIM Ver| Type  | Addr length   |           Checksum            |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |             Unicast-Upstream Neighbor Address                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |  Reserved     | Num groups    |          Holdtime             |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |            Encoded-Multicast Group Address-1                  |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |   Number of Joined  Sources   |   Number of Pruned Sources    |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Joined Source Address-1                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                             .                                 |
         *  |                             .                                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Joined Source Address-n                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Pruned Source Address-1                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                             .                                 |
         *  |                             .                                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |               Encoded-Pruned Source Address-n                 |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                           .                                   |
         *  |                           .                                   |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |                Encoded-Multicast Group Address-n              |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         */

	    {
		uint8_t ngroup;
		uint16_t holdtime;
		uint16_t njoin;
		uint16_t nprune;
		int i, j;

		bp += 4; len -= 4;
		if (PIM_TYPE(pim->pim_typever) != 7) {	/*not for Graft-ACK*/
			if (bp >= ep)
				break;
			ND_PRINT((ndo, ", upstream-neighbor: "));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
				ND_PRINT((ndo, "..."));
				break;
			}
			bp += advance; len -= advance;
		}
		if (bp + 4 > ep)
			break;
		ngroup = bp[1];
		holdtime = EXTRACT_16BITS(&bp[2]);
		ND_PRINT((ndo, "\n\t  %u group(s)", ngroup));
		if (PIM_TYPE(pim->pim_typever) != 7) {	/*not for Graft-ACK*/
			ND_PRINT((ndo, ", holdtime: "));
			if (holdtime == 0xffff)
				ND_PRINT((ndo, "infinite"));
			else
				unsigned_relts_print(ndo, holdtime);
		}
		bp += 4; len -= 4;
		for (i = 0; i < ngroup; i++) {
			if (bp >= ep)
				goto jp_done;
			ND_PRINT((ndo, "\n\t    group #%u: ", i+1));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
				ND_PRINT((ndo, "...)"));
				goto jp_done;
			}
			bp += advance; len -= advance;
			if (bp + 4 > ep) {
				ND_PRINT((ndo, "...)"));
				goto jp_done;
			}
			njoin = EXTRACT_16BITS(&bp[0]);
			nprune = EXTRACT_16BITS(&bp[2]);
			ND_PRINT((ndo, ", joined sources: %u, pruned sources: %u", njoin, nprune));
			bp += 4; len -= 4;
			for (j = 0; j < njoin; j++) {
				ND_PRINT((ndo, "\n\t      joined source #%u: ", j+1));
				if ((advance = pimv2_addr_print(ndo, bp, pimv2_source, 0)) < 0) {
					ND_PRINT((ndo, "...)"));
					goto jp_done;
				}
				bp += advance; len -= advance;
			}
			for (j = 0; j < nprune; j++) {
				ND_PRINT((ndo, "\n\t      pruned source #%u: ", j+1));
				if ((advance = pimv2_addr_print(ndo, bp, pimv2_source, 0)) < 0) {
					ND_PRINT((ndo, "...)"));
					goto jp_done;
				}
				bp += advance; len -= advance;
			}
		}
	jp_done:
		break;
	    }

	case PIMV2_TYPE_BOOTSTRAP:
	{
		int i, j, frpcnt;
		bp += 4;

		/* Fragment Tag, Hash Mask len, and BSR-priority */
		if (bp + sizeof(uint16_t) >= ep) break;
		ND_PRINT((ndo, " tag=%x", EXTRACT_16BITS(bp)));
		bp += sizeof(uint16_t);
		if (bp >= ep) break;
		ND_PRINT((ndo, " hashmlen=%d", bp[0]));
		if (bp + 1 >= ep) break;
		ND_PRINT((ndo, " BSRprio=%d", bp[1]));
		bp += 2;

		/* Encoded-Unicast-BSR-Address */
		if (bp >= ep) break;
		ND_PRINT((ndo, " BSR="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;

		for (i = 0; bp < ep; i++) {
			/* Encoded-Group Address */
			ND_PRINT((ndo, " (group%d: ", i));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0))
			    < 0) {
				ND_PRINT((ndo, "...)"));
				goto bs_done;
			}
			bp += advance;

			/* RP-Count, Frag RP-Cnt, and rsvd */
			if (bp >= ep) {
				ND_PRINT((ndo, "...)"));
				goto bs_done;
			}
			ND_PRINT((ndo, " RPcnt=%d", bp[0]));
			if (bp + 1 >= ep) {
				ND_PRINT((ndo, "...)"));
				goto bs_done;
			}
			ND_PRINT((ndo, " FRPcnt=%d", frpcnt = bp[1]));
			bp += 4;

			for (j = 0; j < frpcnt && bp < ep; j++) {
				/* each RP info */
				ND_PRINT((ndo, " RP%d=", j));
				if ((advance = pimv2_addr_print(ndo, bp,
								pimv2_unicast,
								0)) < 0) {
					ND_PRINT((ndo, "...)"));
					goto bs_done;
				}
				bp += advance;

				if (bp + 1 >= ep) {
					ND_PRINT((ndo, "...)"));
					goto bs_done;
				}
				ND_PRINT((ndo, ",holdtime="));
				unsigned_relts_print(ndo, EXTRACT_16BITS(bp));
				if (bp + 2 >= ep) {
					ND_PRINT((ndo, "...)"));
					goto bs_done;
				}
				ND_PRINT((ndo, ",prio=%d", bp[2]));
				bp += 4;
			}
			ND_PRINT((ndo, ")"));
		}
	   bs_done:
		break;
	}
	case PIMV2_TYPE_ASSERT:
		bp += 4; len -= 4;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " group="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		if (bp >= ep)
			break;
		ND_PRINT((ndo, " src="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance; len -= advance;
		if (bp + 8 > ep)
			break;
		if (bp[0] & 0x80)
			ND_PRINT((ndo, " RPT"));
		ND_PRINT((ndo, " pref=%u", EXTRACT_32BITS(&bp[0]) & 0x7fffffff));
		ND_PRINT((ndo, " metric=%u", EXTRACT_32BITS(&bp[4])));
		break;

	case PIMV2_TYPE_CANDIDATE_RP:
	{
		int i, pfxcnt;
		bp += 4;

		/* Prefix-Cnt, Priority, and Holdtime */
		if (bp >= ep) break;
		ND_PRINT((ndo, " prefix-cnt=%d", bp[0]));
		pfxcnt = bp[0];
		if (bp + 1 >= ep) break;
		ND_PRINT((ndo, " prio=%d", bp[1]));
		if (bp + 3 >= ep) break;
		ND_PRINT((ndo, " holdtime="));
		unsigned_relts_print(ndo, EXTRACT_16BITS(&bp[2]));
		bp += 4;

		/* Encoded-Unicast-RP-Address */
		if (bp >= ep) break;
		ND_PRINT((ndo, " RP="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;

		/* Encoded-Group Addresses */
		for (i = 0; i < pfxcnt && bp < ep; i++) {
			ND_PRINT((ndo, " Group%d=", i));
			if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0))
			    < 0) {
				ND_PRINT((ndo, "..."));
				break;
			}
			bp += advance;
		}
		break;
	}

	case PIMV2_TYPE_PRUNE_REFRESH:
		ND_PRINT((ndo, " src="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;
		ND_PRINT((ndo, " grp="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_group, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;
		ND_PRINT((ndo, " forwarder="));
		if ((advance = pimv2_addr_print(ndo, bp, pimv2_unicast, 0)) < 0) {
			ND_PRINT((ndo, "..."));
			break;
		}
		bp += advance;
		ND_TCHECK2(bp[0], 2);
		ND_PRINT((ndo, " TUNR "));
		unsigned_relts_print(ndo, EXTRACT_16BITS(bp));
		break;

	 default:
		ND_PRINT((ndo, " [type %d]", PIM_TYPE(pim->pim_typever)));
		break;
	}

	return;

trunc:
	ND_PRINT((ndo, "[|pim]"));
}
```

Why it is not vulnerable: The TLV length validation is enforced at two levels: (1) explicit length checks within each option type case validate olen matches the expected size for that type, and (2) ND_TCHECK2(bp[0], 4 + olen) at the start of the while loop ensures the entire option (header + value) fits within the packet before any option-specific processing occurs. For options where explicit length validation is absent (ADDRESS_LIST, default unknown options), the ND_TCHECK2 check still prevents out-of-bounds reads because it validates 4 + olen bytes exist in the packet. Additionally, pimv2_addr_print performs its own internal ND_TCHECK validation before reading address bytes. All other PIM message types use explicit pre-read bounds checks (bp >= ep, bp + N > ep) before accessing fields.

Security invariants:
- TLV option length must not cause buffer over-read: enforced by ND_TCHECK2(bp[0], 4 + olen) at the start of the HELLO option parsing loop, which validates that the option header (4 bytes) plus value (olen bytes) all exist in the packet before proceeding
- TLV option length must match expected size for known types: enforced by explicit olen checks in each switch case - HOLDTIME requires olen == 2, LANPRUNEDELAY requires olen == 4, GENID requires olen == 4, REFRESH_CAP requires olen == 4, DR_PRIORITY accepts olen 0 or 4
- Address field reads must be bounds-checked: enforced by ND_TCHECK and ND_TCHECK2 macros within pimv2_addr_print, which validate the address header and body before reading
- All field accesses must be preceded by bounds validation: enforced by explicit checks like bp >= ep, bp + 4 > ep, bp + 8 > ep throughout the function for non-HELLO message types
- Checksum verification must validate data availability: enforced by ND_TTEST2(bp[0], len) in pimv2_check_checksum before computing checksum over the packet data
- Endpoint clamping must prevent reads past packet end: enforced by if (ep > bp + len) ep = bp + len near function entry

Invariant verification:
- TLV option total size (header + value) validated before processing: holds=true. Evidence: ND_TCHECK2(bp[0], 4 + olen) is called immediately after extracting otype and olen, before any option-specific code executes. This ensures the entire option fits in the packet.
- HOLDTIME option length validated to be exactly 2 bytes: holds=true. Evidence: case PIMV2_HELLO_OPTION_HOLDTIME: if (olen != 2) { ND_PRINT((ndo, "ERROR: Option Length != 2 Bytes (%u)", olen)); } else { unsigned_relts_print(ndo, EXTRACT_16BITS(bp)); }
- LANPRUNEDELAY option length validated to be exactly 4 bytes: holds=true. Evidence: case PIMV2_HELLO_OPTION_LANPRUNEDELAY: if (olen != 4) { ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen)); } else { ... reads bp and bp+2 }
- DR_PRIORITY option length validated to be 0 or 4 bytes: holds=true. Evidence: case PIMV2_HELLO_OPTION_DR_PRIORITY_OLD/DR_PRIORITY: switch (olen) { case 0: ... break; case 4: EXTRACT_32BITS(bp); break; default: ND_PRINT error }
- GENID option length validated to be exactly 4 bytes: holds=true. Evidence: case PIMV2_HELLO_OPTION_GENID: if (olen != 4) { ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen)); } else { ND_PRINT((ndo, "0x%08x", EXTRACT_32BITS(bp))); }
- REFRESH_CAP option length validated to be exactly 4 bytes: holds=true. Evidence: case PIMV2_HELLO_OPTION_REFRESH_CAP: if (olen != 4) { ND_PRINT((ndo, "ERROR: Option Length != 4 Bytes (%u)", olen)); } else { reads *bp, *(bp+1), EXTRACT_16BITS(bp+2) }
- Unknown/default option type data read bounded by packet length: holds=true. Evidence: The ND_TCHECK2(bp[0], 4 + olen) check at loop entry ensures olen bytes exist before print_unknown_data(ndo, bp, "...", olen) is called in the default case or after the switch
- ADDRESS_LIST option iteration bounded correctly: holds=true. Evidence: The while (ptr < (bp+olen)) loop is protected by the earlier ND_TCHECK2(bp[0], 4 + olen), and pimv2_addr_print performs internal ND_TCHECK validation. The break on advance < 0 prevents infinite loops.
- Non-HELLO message field accesses preceded by bounds checks: holds=true. Evidence: Register-Stop: bp >= ep checks before each pimv2_addr_print; Join-Prune: bp + 4 > ep before reading ngroup/holdtime; Bootstrap: bp + sizeof(uint16_t) >= ep and bp + 1 >= ep checks before reads; Assert: bp + 8 > ep before reading preference/metric

Residual risk notes: The BIDIR_CAP option (case PIMV2_HELLO_OPTION_BIDIR_CAP) has no length validation and simply breaks. However, the ND_TCHECK2 at loop entry already ensures the option data fits in the packet, so this is not exploitable. The print_unknown_data function is trusted to not exceed olen bytes, as it is only called after ND_TCHECK2 validates that length. The implicit assumption is that ND_TCHECK2 properly detects buffer truncation and jumps to the trunc label, which is standard tcpdump behavior.

## Verdict: **Real vulnerability (out-of-bounds read / crash via missing bounds check)**

The function `pimv2_print()` can dereference packet data beyond the captured buffer in the **PIMv2 REGISTER** case because it reads `IP_V(ip)` without first verifying that at least one byte of the encapsulated IP header is present.

That is a classic tcpdump-style issue: a crafted/truncated packet can trigger an out-of-bounds read and cause tcpdump to crash (DoS).

---

## Where it happens (code-grounded)

In `case PIMV2_TYPE_REGISTER:`:

```c
bp += 8; len -= 8;
/* encapsulated multicast packet */
ip = (const struct ip *)bp;
switch (IP_V(ip)) {
...
}
```

Before calling `IP_V(ip)`, there is **no check** that `bp < ep` (or that at least 1 byte is available). `IP_V(ip)` reads the first byte of the IPv4 header (`ip_vhl`) to extract the version nibble. If `bp == ep` (or `bp > ep`), this dereference is an OOB read.

There *is* an earlier check:

```c
ND_TCHECK2(*(bp + 4), PIMV2_REGISTER_FLAG_LEN);
...
bp += 8; len -= 8;
```

That ensures the **register flags area** (at offset `bp+4`, length `PIMV2_REGISTER_FLAG_LEN`) is present, but it **does not** ensure any bytes exist after the 8-byte PIM Register header (i.e., for the encapsulated packet).

So a packet can be long enough to include the flags field but contain **no encapsulated IP header bytes**, and the code still reads `IP_V(ip)`.

---

## Exploitability / attack preconditions

**Attacker control:** Packet contents and truncation are attacker-controlled if tcpdump is sniffing live traffic (or reading a crafted pcap).

**Trigger condition:** A PIMv2 Register message where the captured length permits the flags check but not even 1 byte of the encapsulated packet. Concretely, after `bp += 8`, `bp` can equal `ep`.

**Result:** Attempting to evaluate `IP_V(ip)` reads past `ndo->ndo_snapend` → likely crash depending on platform/memory layout. At minimum it’s an out-of-bounds read.

This is realistically a **denial-of-service** against tcpdump.

---

## Why existing guards don’t prevent it

* `ND_TCHECK2(*(bp + 4), PIMV2_REGISTER_FLAG_LEN)` only validates bytes starting at `bp+4`.
* There is no subsequent `ND_TCHECK`/`ND_TCHECK2` for the encapsulated packet before inspecting its version.

By contrast, other branches tend to do `if (bp >= ep) break;` before parsing further, but this particular path does not.

---

## Classification

* **CWE-125: Out-of-bounds Read**
* Impact: **Crash/DoS** of tcpdump (and potentially information disclosure in some contexts, but tcpdump generally just reads and prints; the primary practical impact is DoS).

---

## Minimal fix (conceptual)

Add a bounds check after advancing past the register header, before `IP_V(ip)`:

```c
bp += 8; len -= 8;
if (bp >= ep)      /* or ND_TCHECK(bp[0]) */
    goto trunc;    /* or break with trunc-style output */
```

Even better: ensure enough bytes for at least the first byte, and ideally enough for the minimum header size before calling `ip_print`/`ip6_print` (though those functions may do their own checks).

---

## Residual notes

Other parts of the function use `ND_TCHECK*` and `ep` comparisons reasonably, and `pimv2_check_checksum()` defends itself with `ND_TTEST2(bp[0], len)`. The standout issue here is specifically the **missing check before `IP_V(ip)`** in the Register case.

No ND_TCHECK/ND_TTEST or (bp >= ep) guard before dereferencing IP_V(ip).

After validating only the Register flags area (bytes at bp+4 for PIMV2_REGISTER_FLAG_LEN), the code advances bp by 8 and immediately reads the first byte of the encapsulated packet via IP_V(ip) without ensuring at least 1 byte remains within ndo->ndo_snapend (ep). This permits an out-of-bounds read on truncated/crafted captures.

Remote DoS plausible. An attacker who can inject PIMv2 Register traffic (or supply a crafted pcap) can cause tcpdump to read beyond the captured buffer and potentially crash, depending on memory/layout and build options.

tcpdump processes a PIMv2 Register message (PIM_TYPE == PIMV2_TYPE_REGISTER)

Capture/truncation such that bytes for the flags field are present (satisfying ND_TCHECK2(*(bp+4), PIMV2_REGISTER_FLAG_LEN)), but no bytes (or insufficient bytes) exist after the 8-byte Register header

Typical scenario: small snaplen, truncated pcap, or intentionally crafted minimal-length packet

Out-of-bounds read when evaluating IP_V(ip)

Likely tcpdump crash (denial of service) when run on malicious traffic/pcap

ep is set to min(ndo->ndo_snapend, bp+len) early in pimv2_print

Many parsing branches check (bp >= ep) or use ND_TCHECK/ND_TCHECK2 before dereferences

pimv2_check_checksum uses ND_TTEST2 to avoid OOB reads during checksum calculation

In the Register case, the only pre-dereference bounds check is ND_TCHECK2(*(bp+4), PIMV2_REGISTER_FLAG_LEN), which covers the flags area within the 8-byte header but does not imply any availability for the encapsulated payload. After bp += 8, no check enforces bp < ep before IP_V(ip) reads the first byte of the next header.

Downstream ip_print/ip6_print likely perform their own bounds checks, but the vulnerability triggers before those calls (at IP_V(ip)).

Depending on platform and compiler, the OOB read may not always crash, but it is still an invalid memory access and a stability risk.

main (tcpdump.c)
pim_print (print-pim.c)
pimv2_print (print-pim.c)