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/ImageMagick/ImageMagick
Language: C
File: coders/wpg.c
Function: ExtractPostscript

Call path: ReadWPGImage (coders/wpg.c) → ExtractPostscript (coders/wpg.c) → GetMagicInfo (MagickCore/magic.c) → ReadImage (MagickCore/image.c)

### Primary Function

```c
static Image *ExtractPostscript(Image *image,const ImageInfo *image_info,
  MagickOffsetType PS_Offset,ssize_t PS_Size,ExceptionInfo *exception)
{
  char
    postscript_file[MaxTextExtent];

  const MagicInfo
    *magic_info;

  FILE
    *ps_file;

  ImageInfo
    *clone_info;

  Image
    *image2;

  unsigned char
    magick[2*MaxTextExtent];

  if ((clone_info=CloneImageInfo(image_info)) == NULL)
    return(image);
  clone_info->blob=(void *) NULL;
  clone_info->length=0;

  /* Obtain temporary file */
  (void) AcquireUniqueFilename(postscript_file);
  ps_file=fopen_utf8(postscript_file,"wb");
  if (ps_file == (FILE *) NULL)
    goto FINISH;

  /* Copy postscript to temporary file */
  (void) SeekBlob(image,PS_Offset,SEEK_SET);
  (void) ReadBlob(image, 2*MaxTextExtent, magick);

  (void) SeekBlob(image,PS_Offset,SEEK_SET);
  while(PS_Size-- > 0)
    {
      (void) fputc(ReadBlobByte(image),ps_file);
    }
  (void) fclose(ps_file);

    /* Detect file format - Check magic.mgk configuration file. */
  magic_info=GetMagicInfo(magick,2*MaxTextExtent,exception);
  if(magic_info == (const MagicInfo *) NULL) goto FINISH_UNL;
  /*     printf("Detected:%s  \n",magic_info->name); */
  if(exception->severity != UndefinedException) goto FINISH_UNL;
  if(magic_info->name == (char *) NULL) goto FINISH_UNL;

  (void) strncpy(clone_info->magick,magic_info->name,MaxTextExtent);

    /* Read nested image */
  /*FormatString(clone_info->filename,"%s:%s",magic_info->name,postscript_file);*/
  FormatLocaleString(clone_info->filename,MaxTextExtent,"%s",postscript_file);
  image2=ReadImage(clone_info,exception);

  if (!image2)
    goto FINISH_UNL;

  /*
    Replace current image with new image while copying base image
    attributes.
  */
  (void) CopyMagickString(image2->filename,image->filename,MaxTextExtent);
  (void) CopyMagickString(image2->magick_filename,image->magick_filename,MaxTextExtent);
  (void) CopyMagickString(image2->magick,image->magick,MaxTextExtent);
  image2->depth=image->depth;
  DestroyBlob(image2);
  image2->blob=ReferenceBlob(image->blob);

  if ((image->rows == 0) || (image->columns == 0))
    DeleteImageFromList(&image);

  AppendImageToList(&image,image2);

 FINISH_UNL:
  (void) RelinquishUniqueFileResource(postscript_file);
 FINISH:
  DestroyImageInfo(clone_info);
  return(image);
}
```

### Cross-File Context

[MaxTextExtent — macro — MagickCore/method-attribute.h:80]
MaxTextExtent → MagickPathExtent  (MagickCore/method-attribute.h:80)

[MagickPathExtent — macro — MagickCore/method-attribute.h:79]
MagickPathExtent → 4096  (MagickCore/method-attribute.h:79)

[_MagicInfo — struct — MagickCore/magic.c:82]
```c
struct _MagicInfo
{
  char
    *name;

  unsigned char
    *magic;

  size_t
    length;

  MagickOffsetType
    offset;

  MagickBooleanType
    skip_spaces;

  size_t
    signature;
};
```

[_ImageInfo — struct — MagickCore/image.h:358]
```c
struct _ImageInfo
{
  ...
  char
    magick[MagickPathExtent],    /* image file format (file magick) */
  ...
};
```

[GetMagicInfo — callee — MagickCore/magic.c:303]
```c
MagickExport const MagicInfo *GetMagicInfo(const unsigned char *magic,
  const size_t length,ExceptionInfo *exception)
```

Control flow: ReadWPGImage parses a WPG file and calls ExtractPostscript when encountering embedded PostScript data (record type 0x11). ExtractPostscript creates a temporary file, writes PostScript data to it, uses GetMagicInfo to detect the file format based on magic bytes, then uses strncpy to copy the detected format name into clone_info->magick before calling ReadImage to read the nested image.

Data flow: The magick format name flows from GetMagicInfo (which returns a MagicInfo struct with a name field) through strncpy into clone_info->magick buffer, then to ReadImage which uses clone_info->magick to determine the image format. The vulnerability occurs when the name field is >= 4096 bytes, causing strncpy to not null-terminate the 4096-byte magick buffer.

Verdict: Vulnerable
Vulnerable region:
[coders/wpg.c]
```c
/*     printf("Detected:%s  \n",magic_info->name); */
```
CWE: CWE-189
Severity: MEDIUM

Root cause: The strncpy at line 806 in coders/wpg.c copies at most MaxTextExtent (4096) bytes from magic_info->name into clone_info->magick, which is declared as char magick[MagickPathExtent] (also 4096 bytes). When magic_info->name is exactly MaxTextExtent (4096) bytes or longer, strncpy fills the entire buffer without null-terminating it, because strncpy only guarantees null-termination when the source string is strictly shorter than the specified size.

Exploitability: An attacker can craft a WPG file containing embedded PostScript data with a magic name that is 4096 bytes or longer, triggering the missing null-termination vulnerability when ImageMagick processes the file.

Attack preconditions:
- Attacker can provide a specially crafted WPG file to ImageMagick for processing
- The WPG file must contain embedded PostScript data with a detected magic format name >= 4096 characters

Likely impact: An attacker can craft a WPG file containing embedded PostScript data with a magic name that is 4096 bytes or longer, triggering the missing null-termination vulnerability when ImageMagick processes the file.

ReadWPGImage ExtractPostscript GetMagicInfo ReadImage

**Verdict: likely real vulnerability — uncontrolled resource consumption / DoS**

### Observed code behavior

`ExtractPostscript()` takes two key inputs:

- `PS_Offset`: where the embedded PostScript starts in the WPG blob
- `PS_Size`: how many bytes to extract

It then:

1. Seeks to `PS_Offset`
2. Reads up to `2*MaxTextExtent` bytes into `magick` for format detection
3. Seeks again to `PS_Offset`
4. Copies **exactly `PS_Size` iterations** into a temp file:

```c
while(PS_Size-- > 0)
{
  (void) fputc(ReadBlobByte(image),ps_file);
}
```

After that it uses the extracted temp file with `ReadImage()`.

---

## Why this is vulnerable

### 1. `PS_Size` is trusted without validating source bounds

There is **no check** that:

- `PS_Offset` is within the blob, or
- `PS_Offset + PS_Size` fits within the actual input length.

So if an attacker can control the WPG metadata that produces `PS_Size`, they can make it much larger than the actual embedded data.

### 2. EOF / read failure is ignored

The loop writes one byte per iteration regardless of whether `ReadBlobByte(image)` successfully read a byte.

The return value of `ReadBlobByte(image)` is passed directly to `fputc(...)` and ignored:

```c
(void) fputc(ReadBlobByte(image),ps_file);
```

If the source blob is exhausted, typical blob APIs return EOF / `-1`. `fputc(-1, ps_file)` still writes a byte value (`0xFF`) unless it itself fails. So after actual input ends, the loop can continue manufacturing bytes into the temp file until `PS_Size` reaches zero.

### 3. Result: attacker can force creation of a huge temporary file

That means a small malicious WPG can cause ImageMagick to write a temp file whose size is the attacker-declared `PS_Size`, not the real embedded object size.

Impact:

- disk exhaustion
- CPU time consumption
- follow-on memory / parser work when `ReadImage()` processes the oversized temp file

This is a practical denial of service.

---

## Exploitability

### Preconditions
A realistic attacker needs to supply a crafted WPG file where the parsed `PS_Size` is attacker-controlled or insufficiently validated upstream.

### Attack shape
Example:

- actual file contains only a few bytes after `PS_Offset`
- `PS_Size` is set to a huge value

Then this loop will keep writing bytes to the temp file for all iterations, even after input ends.

If `PS_Size` is large enough, this can consume substantial disk space or hang processing.

### Why guards do not stop it
The function has no guard on:

- blob length remaining
- `ReadBlobByte()` success
- temp file size limit

So the critical loop is not bounded by the real source data.

---

## Precise vulnerability classification

Best fit:

- **CWE-400: Uncontrolled Resource Consumption**
- Also related to **improper input validation / missing bounds validation** on extracted subobject size

---

## Additional observations

### Uninitialized stack bytes in `magick`
This code does:

```c
(void) ReadBlob(image, 2*MaxTextExtent, magick);
magic_info=GetMagicInfo(magick,2*MaxTextExtent,exception);
```

without checking how many bytes were actually read. If fewer than `2*MaxTextExtent` bytes are available, part of `magick` may remain uninitialized and still be passed to `GetMagicInfo()`.

That is sloppy and could cause misdetection/undefined behavior, but from the supplied code it is **not the strongest security issue**. The clear, directly exploitable issue is the unbounded extraction loop.

### Temporary file race
The pattern:

```c
AcquireUniqueFilename(postscript_file);
ps_file=fopen_utf8(postscript_file,"wb");
```

can be dangerous if `AcquireUniqueFilename()` only generates a name rather than atomically creating the file. But that helper’s semantics are not provided here, so I would **not** call that a confirmed vulnerability from this snippet alone.

---

## Conclusion

**Yes, this code likely contains a real vulnerability.**

The function trusts `PS_Size` and writes exactly that many bytes to a temp file without verifying that the source blob actually contains that much data, and without checking `ReadBlobByte()` success. A crafted WPG can therefore cause **arbitrary temporary file growth** and likely **denial of service** via disk/CPU exhaustion.

If you want, I can also suggest a minimal safe patch pattern for this function.

Creates a temporary file, seeks to PS_Offset in the source blob, copies PS_Size bytes byte-by-byte into the temp file, detects format from an initial buffer, and invokes ReadImage() on the extracted file.

The loop is bounded only by attacker-influenced PS_Size, not by the actual remaining blob length or successful reads.

ExtractPostscript trusts PS_Size and PS_Offset without validating them against the source blob size, and ignores read failure/EOF from ReadBlobByte(). As a result, the function continues writing one byte per iteration to the temporary file even after the source data is exhausted.

An attacker who can supply a crafted WPG file that causes a large PS_Size value can force ImageMagick to generate a large temporary file and spend substantial CPU time in the extraction loop. This is realistically exploitable as denial of service.

The attacker can supply or influence a WPG file processed by ReadWPGImage.

The WPG parser path populates PS_Offset and PS_Size from file-controlled metadata.

Temporary file creation is permitted on the host filesystem.

Disk space exhaustion from oversized temporary file creation.

CPU consumption from looping over a large PS_Size.

Potential downstream parser work on a fabricated oversized extracted file via ReadImage().

clone_info->magick is written with strncpy using MaxTextExtent, matching the destination buffer size in ImageInfo.

Temporary file resource is relinquished on both normal and error paths after creation.

The code checks for NULL returns from CloneImageInfo(), fopen_utf8(), GetMagicInfo(), and ReadImage().

The string copy into clone_info->magick is size-bounded by MaxTextExtent, and the struct field is also MagickPathExtent/MaxTextExtent sized.

RelinquishUniqueFileResource(postscript_file) is reached through FINISH_UNL after temp-file use.

However, none of these guards constrain PS_Size, validate PS_Offset + PS_Size against blob length, or stop the copy loop on EOF.

The initial ReadBlob(image, 2*MaxTextExtent, magick) does not check how many bytes were actually read before passing the full buffer length to GetMagicInfo(), so format detection may inspect partially uninitialized stack data. From the supplied code, this is secondary to the resource-consumption issue.

The AcquireUniqueFilename() then fopen_utf8() pattern could be risky if filename reservation is non-atomic, but the provided context does not establish that as a confirmed vulnerability.

ReadWPGImage (coders/wpg.c)
ExtractPostscript (coders/wpg.c)
GetMagicInfo (MagickCore/magic.c)
ReadImage (MagickCore/image.c)


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: github.com/torvalds/linux
Language: C
File: kernel/sys.c
Function: do_prlimit

Call path: sys_prlimit64 (kernel/sys.c) → do_prlimit (kernel/sys.c) → sys_setrlimit (kernel/sys.c)

### Primary Function

```c
static int do_prlimit(struct task_struct *tsk, unsigned int resource,
		      struct rlimit *new_rlim, struct rlimit *old_rlim)
{
	struct rlimit *rlim;
	int retval = 0;

	if (resource >= RLIM_NLIMITS)
		return -EINVAL;
	resource = array_index_nospec(resource, RLIM_NLIMITS);

	if (new_rlim) {
		if (new_rlim->rlim_cur > new_rlim->rlim_max)
			return -EINVAL;
		if (resource == RLIMIT_NOFILE &&
				new_rlim->rlim_max > sysctl_nr_open)
			return -EPERM;
	}

	/* Holding a refcount on tsk protects tsk->signal from disappearing. */
	rlim = tsk->signal->rlim + resource;
	task_lock(tsk->group_leader);
	if (new_rlim) {
		/*
		 * Keep the capable check against init_user_ns until cgroups can
		 * contain all limits.
		 */
		if (new_rlim->rlim_max > rlim->rlim_max &&
				!capable(CAP_SYS_RESOURCE))
			retval = -EPERM;
		if (!retval)
			retval = security_task_setrlimit(tsk, resource, new_rlim);
	}
	if (!retval) {
		if (old_rlim)
			*old_rlim = *rlim;
		if (new_rlim)
			*rlim = *new_rlim;
	}
	task_unlock(tsk->group_leader);

	/*
	 * RLIMIT_CPU handling. Arm the posix CPU timer if the limit is not
	 * infinite. In case of RLIM_INFINITY the posix CPU timer code
	 * ignores the rlimit.
	 */
	if (!retval && new_rlim && resource == RLIMIT_CPU &&
	    new_rlim->rlim_cur != RLIM_INFINITY &&
	    IS_ENABLED(CONFIG_POSIX_TIMERS)) {
		/*
		 * update_rlimit_cpu can fail if the task is exiting, but there
		 * may be other tasks in the thread group that are not exiting,
		 * and they need their cpu timers adjusted.
		 *
		 * The group_leader is the last task to be released, so if we
		 * cannot update_rlimit_cpu on it, then the entire process is
		 * exiting and we do not need to update at all.
		 */
		update_rlimit_cpu(tsk->group_leader, new_rlim->rlim_cur);
	}

	return retval;
}
```

### Cross-File Context

[RLIM_NLIMITS — constant — include/uapi/asm-generic/resource.h:49]
RLIM_NLIMITS → 16  (include/uapi/asm-generic/resource.h:49)

[array_index_nospec — macro — include/linux/nospec.h:51]
array_index_nospec → #define array_index_nospec(index, size) \ ({ \ typeof(index) _i = (index); \ typeof(size) _s = (size); \ unsigned long _mask = array_index_mask_nospec(_i, _s); \ \ BUILD_BUG_ON(sizeof(_i) > sizeof(long)); \ BUILD_BUG_ON(sizeof(_s) > sizeof(long)); \ \ (typeof(_i)) (_i & _mask); \ })  (include/linux/nospec.h:51)

[array_index_mask_nospec — function — include/linux/nospec.h:24]
```c
static inline unsigned long array_index_mask_nospec(unsigned long index,
					    unsigned long size)
{
	/*
	 * Always calculate and emit the mask even if the compiler
	 * thinks the mask is not needed. The compiler does not take
	 * into account the value of @index under speculation.
	 */
	OPTIMIZER_HIDE_VAR(index);
	return ~(long)(index | (size - 1UL - index)) >> (BITS_PER_LONG - 1);
}
```

[struct rlimit — struct — include/uapi/linux/resource.h:43]
```c
struct rlimit {
	__kernel_ulong_t	rlim_cur;
	__kernel_ulong_t	rlim_max;
};
```

[tsk->signal->rlim — field — include/linux/sched/signal.h:213]
struct rlimit rlim[RLIM_NLIMITS];

Provide the final verdict as strict JSON.

Return only one JSON object and no surrounding prose.

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

Control flow: The function begins by validating the resource index against RLIM_NLIMITS, returning -EINVAL if out of bounds. It then applies array_index_nospec to sanitize the index against speculative execution. If new_rlim is provided, it validates that rlim_cur <= rlim_max (returns -EINVAL if not) and enforces sysctl_nr_open for RLIMIT_NOFILE (returns -EPERM). The function then accesses the per-signal rlim array using the sanitized index, acquires task_lock on the group_leader, and performs capability checks (CAP_SYS_RESOURCE) and LSM hooks (security_task_setrlimit). If no errors occurred, it copies the old limit to old_rlim (if provided) and writes the new limit to the rlim array. The lock is released. Finally, for RLIMIT_CPU with a finite value and CONFIG_POSIX_TIMERS, it calls update_rlimit_cpu on the group_leader. The function returns the accumulated error code.

Data flow: The resource parameter (user-controlled via sys_prlimit64 call path) is first bounds-checked with resource >= RLIM_NLIMITS, then sanitized via array_index_nospec(resource, RLIM_NLIMITS) which masks the index to be zero if out-of-bounds. The sanitized resource index is used to index into tsk->signal->rlim[RLIM_NLIMITS]. The new_rlim and old_rlim pointers conditionally flow into comparisons, capability/security checks, and memory copies. The tsk parameter flows through task_lock/task_unlock for synchronization and is used to access signal->rlim.

Verdict: Not Vulnerable

Reviewed region:
[kernel/sys.c]
```c
static int do_prlimit(struct task_struct *tsk, unsigned int resource,
		      struct rlimit *new_rlim, struct rlimit *old_rlim)
{
	struct rlimit *rlim;
	int retval = 0;

	if (resource >= RLIM_NLIMITS)
		return -EINVAL;
	resource = array_index_nospec(resource, RLIM_NLIMITS);

	if (new_rlim) {
		if (new_rlim->rlim_cur > new_rlim->rlim_max)
			return -EINVAL;
		if (resource == RLIMIT_NOFILE &&
				new_rlim->rlim_max > sysctl_nr_open)
			return -EPERM;
	}

	/* Holding a refcount on tsk protects tsk->signal from disappearing. */
	rlim = tsk->signal->rlim + resource;
	task_lock(tsk->group_leader);
	if (new_rlim) {
		/*
		 * Keep the capable check against init_user_ns until cgroups can
		 * contain all limits.
		 */
		if (new_rlim->rlim_max > rlim->rlim_max &&
				!capable(CAP_SYS_RESOURCE))
			retval = -EPERM;
		if (!retval)
			retval = security_task_setrlimit(tsk, resource, new_rlim);
	}
	if (!retval) {
		if (old_rlim)
			*old_rlim = *rlim;
		if (new_rlim)
			*rlim = *new_rlim;
	}
	task_unlock(tsk->group_leader);

	/*
	 * RLIMIT_CPU handling. Arm the posix CPU timer if the limit is not
	 * infinite. In case of RLIM_INFINITY the posix CPU timer code
	 * ignores the rlimit.
	 */
	if (!retval && new_rlim && resource == RLIMIT_CPU &&
	    new_rlim->rlim_cur != RLIM_INFINITY &&
	    IS_ENABLED(CONFIG_POSIX_TIMERS)) {
		/*
		 * update_rlimit_cpu can fail if the task is exiting, but there
		 * may be other tasks in the thread group that are not exiting,
		 * and they need their cpu timers adjusted.
		 *
		 * The group_leader is the last task to be released, so if we
		 * cannot update_rlimit_cpu on it, then the entire process is
		 * exiting and we do not need to update at all.
		 */
		update_rlimit_cpu(tsk->group_leader, new_rlim->rlim_cur);
	}

	return retval;
}
```

Why it is not vulnerable: The code protects the array access `tsk->signal->rlim + resource` against speculative out-of-bounds execution through two mechanisms: (1) a standard bounds check `if (resource >= RLIM_NLIMITS) return -EINVAL;` that prevents actual out-of-bounds access, and (2) `resource = array_index_nospec(resource, RLIM_NLIMITS);` which applies a speculation-resistant mask to the index. The `array_index_nospec` macro uses `OPTIMIZER_HIDE_VAR` to prevent compiler optimization of the branch condition and computes a mask that is all-ones for valid indices and all-zeros for out-of-bounds indices, ensuring the masked index never exceeds bounds even during speculative execution. Thus, the memory access `rlim = tsk->signal->rlim + resource` is properly guarded.

Security invariants:
- The resource index must be within [0, RLIM_NLIMITS) before array access — enforced by `if (resource >= RLIM_NLIMITS) return -EINVAL;`
- The resource index must be masked against speculative bypass of the bounds check — enforced by `resource = array_index_nospec(resource, RLIM_NLIMITS);` which uses OPTIMIZER_HIDE_VAR and arithmetic masking to produce a zero index for out-of-bounds values
- The new rlim_cur must not exceed new rlim_max — enforced by `if (new_rlim->rlim_cur > new_rlim->rlim_max) return -EINVAL;`
- RLIMIT_NOFILE max must not exceed sysctl_nr_open — enforced by the check `new_rlim->rlim_max > sysctl_nr_open` returning -EPERM
- Increasing rlim_max requires CAP_SYS_RESOURCE — enforced by `if (new_rlim->rlim_max > rlim->rlim_max && !capable(CAP_SYS_RESOURCE)) retval = -EPERM;`
- LSM security hooks must approve the change — enforced by `security_task_setrlimit(tsk, resource, new_rlim)`
- Concurrent access to rlim must be serialized — enforced by `task_lock(tsk->group_leader)` / `task_unlock(tsk->group_leader)` surrounding the read and write of rlim
- The rlim array has exactly RLIM_NLIMITS elements — confirmed by `struct rlimit rlim[RLIM_NLIMITS]` in the context

Invariant verification:
- Bounds check on resource index before array access: holds=true. Evidence: The explicit check `if (resource >= RLIM_NLIMITS) return -EINVAL;` returns early if resource is out of range, preventing normal-path out-of-bounds access to `tsk->signal->rlim + resource`
- Speculative execution mitigation via index masking: holds=true. Evidence: `resource = array_index_nospec(resource, RLIM_NLIMITS);` calls array_index_mask_nospec which uses OPTIMIZER_HIDE_VAR to prevent compiler knowledge of the index during speculation, and computes a mask via `~(long)(index | (size - 1UL - index)) >> (BITS_PER_LONG - 1)` that zeros out the index if it is >= size
- RLIMIT_NOFILE does not exceed sysctl_nr_open: holds=true. Evidence: The condition `if (resource == RLIMIT_NOFILE && new_rlim->rlim_max > sysctl_nr_open) return -EPERM;` enforces this constraint before any array write
- Privilege escalation prevention for increasing limits: holds=true. Evidence: The check `if (new_rlim->rlim_max > rlim->rlim_max && !capable(CAP_SYS_RESOURCE)) retval = -EPERM;` prevents unprivileged users from raising the hard limit
- Concurrent access protection via task lock: holds=true. Evidence: `task_lock(tsk->group_leader)` is acquired before reading/writing `*rlim` and `task_unlock(tsk->group_leader)` is called after, with the lock surrounding both the old_rlim read and new_rlim write
- Array size matches bounds check constant: holds=true. Evidence: The context confirms `struct rlimit rlim[RLIM_NLIMITS]` and `RLIM_NLIMITS → 16`, matching the constant used in the bounds check and array_index_nospec call

Residual risk notes: The safety of `array_index_nospec` depends on the correctness of `OPTIMIZER_HIDE_VAR` and the compiler's ability to emit the masking instructions on all supported architectures. The behavioral equivalence of the masking formula `~(long)(index | (size - 1UL - index)) >> (BITS_PER_LONG - 1)` for all unsigned long values is assumed from the Linux kernel implementation. No additional runtime verification of the mask is performed in this function.

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

Verdict: no real vulnerability evident in the supplied code.

Why it looks suspicious
- The function updates per-process resource limits, which are security-relevant.
- It dereferences `tsk->signal->rlim + resource`, so an out-of-bounds index would be dangerous.
- It can raise or lower limits for another task, so missing authorization checks would matter.

But, from the supplied code, the key safety checks are present.

Observed evidence

1. Resource index is bounds-checked before array access
```c
if (resource >= RLIM_NLIMITS)
	return -EINVAL;
resource = array_index_nospec(resource, RLIM_NLIMITS);
...
rlim = tsk->signal->rlim + resource;
```

- `RLIM_NLIMITS` is 16.
- `tsk->signal->rlim` is an array of exactly `RLIM_NLIMITS` entries.
- The normal architectural safety comes from the explicit `resource >= RLIM_NLIMITS` check.
- `array_index_nospec()` then additionally mitigates speculative out-of-bounds use.

So there is no reachable OOB access here under normal execution, and speculation hardening is also applied.

2. New limit values are sanity-checked
```c
if (new_rlim) {
	if (new_rlim->rlim_cur > new_rlim->rlim_max)
		return -EINVAL;
	if (resource == RLIMIT_NOFILE &&
			new_rlim->rlim_max > sysctl_nr_open)
		return -EPERM;
}
```

This prevents:
- soft limit exceeding hard limit
- `RLIMIT_NOFILE` hard limit exceeding the global maximum `sysctl_nr_open`

So user-controlled `new_rlim` values are constrained before being committed.

3. Raising the hard limit requires privilege
```c
if (new_rlim->rlim_max > rlim->rlim_max &&
		!capable(CAP_SYS_RESOURCE))
	retval = -EPERM;
```

This is the main authorization guard inside this function:
- unprivileged callers cannot increase a hard limit above its existing value
- lowering limits or lowering soft limits within the hard limit is allowed

The nearby comment shows this is intentional policy:
```c
/*
 * Keep the capable check against init_user_ns until cgroups can
 * contain all limits.
 */
```

That is a design choice, not evidence of a bug.

4. LSM hook is consulted before applying the change
```c
if (!retval)
	retval = security_task_setrlimit(tsk, resource, new_rlim);
```

Even if the capability check passes, Linux Security Modules can deny the operation.

5. Access to the shared rlimit state is synchronized
```c
task_lock(tsk->group_leader);
...
if (old_rlim)
	*old_rlim = *rlim;
if (new_rlim)
	*rlim = *new_rlim;
task_unlock(tsk->group_leader);
```

The old value is read and the new value is written while holding the task lock, preventing races on the `rlim` structure itself.

6. Lifetime of `tsk->signal` is explicitly protected
```c
/* Holding a refcount on tsk protects tsk->signal from disappearing. */
```

Given the supplied context, this addresses the obvious use-after-free concern around `tsk->signal`.

Security reasoning

Attacker influence
- `resource` and `new_rlim` ultimately come from userspace through `sys_prlimit64`.
- That means attacker-controlled inputs do reach this function.

Relevant guards
- Invalid `resource` values are rejected.
- Speculative misuse of `resource` is mitigated.
- Invalid `rlimit` relationships are rejected.
- Privilege is required to raise hard limits.
- `RLIMIT_NOFILE` has an explicit ceiling.
- An LSM hook can apply additional policy.
- The update is lock-protected.

What is not shown
- This function itself does not check whether the caller is allowed to operate on `tsk` at all.
- However, the supplied call path includes `sys_prlimit64`, and the function signature takes a `struct task_struct *tsk`, implying task selection and cross-task permission checks happen before this helper is called.
- Based only on this snippet, there is no evidence that arbitrary unauthorized target selection is possible.

Potential concerns that are not actual vulnerabilities here

- Missing bounds check on `old_rlim`/`new_rlim` pointers:
  Not applicable in this helper. These are kernel pointers passed by callers after userspace marshaling/copying, not raw userspace pointers in this function.

- TOCTOU around `rlim->rlim_max`:
  The comparison and write occur under the same task lock, so the relevant state is synchronized.

- Speculative execution OOB:
  Explicitly mitigated via `array_index_nospec()` after the architectural bounds check.

Conclusion
From the supplied code and context, `do_prlimit()` does not present a real vulnerability. The dangerous inputs are validated, the array access is safely bounded, privilege checks exist for sensitive changes, LSM policy is enforced, and shared state is updated under lock.

Residual uncertainty
- Full authorization for acting on another task is not shown in this snippet and likely lives in `sys_prlimit64`.
- If upstream caller checks were absent or wrong, that would be a vulnerability in the caller path, not in `do_prlimit()` as shown.

Most precise classification
- No vulnerability demonstrated in the provided function.

Validates resource index and new rlimit values, locks task state, checks privilege for raising hard limits, invokes LSM hook, copies old limit, applies new limit, and updates RLIMIT_CPU timer state after unlock.

No concrete exploitability is shown in the supplied function. User-influenced inputs are checked before use, and the sensitive state change is guarded by privilege and LSM checks.

Caller can reach sys_prlimit64 and supply a target task, resource index, and optional new rlimit.

To raise an existing hard limit, caller would need CAP_SYS_RESOURCE or equivalent policy allowance.

Any cross-task authorization issue would have to exist in the caller path, not be demonstrated by this helper alone.

No direct security impact is established from this function as shown. Invalid resource indices are rejected, out-of-bounds speculative indexing is mitigated, and unauthorized hard-limit increases are denied.

The function rejects resource >= RLIM_NLIMITS before indexing the fixed-size tsk->signal->rlim[RLIM_NLIMITS] array, then applies array_index_nospec() for speculation hardening. It validates new_rlim consistency (rlim_cur <= rlim_max) and caps RLIMIT_NOFILE against sysctl_nr_open. The security-sensitive case of increasing rlim_max above the current hard limit is denied unless capable(CAP_SYS_RESOURCE) succeeds, and security_task_setrlimit() provides an additional LSM policy gate. Access to the shared rlimit state is synchronized with task_lock(tsk->group_leader), and the comment states a held task refcount protects tsk->signal lifetime. The function takes kernel pointers for new_rlim/old_rlim, so it is not directly dereferencing user pointers here.

resource must be < RLIM_NLIMITS before dereferencing tsk->signal->rlim + resource

Speculative execution must not bypass the resource bounds check

new_rlim->rlim_cur cannot exceed new_rlim->rlim_max

RLIMIT_NOFILE hard limit cannot exceed sysctl_nr_open

Raising a hard limit above the current hard limit requires CAP_SYS_RESOURCE

LSM policy must approve setrlimit changes

Reads/writes of the rlimit entry occur while holding task_lock(tsk->group_leader)

tsk->signal lifetime is protected by a task refcount per the function comment

Present: if (resource >= RLIM_NLIMITS) return -EINVAL;

Present: resource = array_index_nospec(resource, RLIM_NLIMITS);

Context shows struct signal_struct has rlim[RLIM_NLIMITS], so the post-check index is in range.

Present: if (new_rlim->rlim_cur > new_rlim->rlim_max) return -EINVAL;

Present: if (resource == RLIMIT_NOFILE && new_rlim->rlim_max > sysctl_nr_open) return -EPERM;

Present under lock: if (new_rlim->rlim_max > rlim->rlim_max && !capable(CAP_SYS_RESOURCE)) retval = -EPERM;

Present: retval = security_task_setrlimit(tsk, resource, new_rlim);

Present around copy/read-modify-write of *rlim with task_lock/task_unlock.

This helper does not itself verify whether the caller is authorized to operate on the chosen target task; that check is expected in sys_prlimit64 or earlier path logic and is not shown here.

The capability check uses capable(CAP_SYS_RESOURCE) as noted by the comment; whether that policy is globally appropriate is a design question, not evidence of a flaw in this helper from the provided code.

The post-unlock RLIMIT_CPU timer adjustment can fail for exiting tasks by design, but the comment explains that this is acceptable and does not indicate a security issue.

sys_prlimit64 (kernel/sys.c)
do_prlimit (kernel/sys.c)
sys_setrlimit (kernel/sys.c)


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/modelcontextprotocol/go-sdk
Language: Go
File: mcp/streamable.go
Function: ServeHTTP

Call path: ServeHTTP (mcp/streamable.go) → http.LocalAddrContextKey (net/http) → util.IsLoopback (internal/util/net.go)

### Primary Function

```go
func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// DNS rebinding protection: auto-enabled for localhost servers.
	// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
	if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" {
		if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil {
			if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) {
				http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden)
				return
			}
		}
	}

	// Allow multiple 'Accept' headers.
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax
	accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",")
	var jsonOK, streamOK bool
	for _, c := range accept {
		switch strings.TrimSpace(c) {
		case "application/json", "application/*":
			jsonOK = true
		case "text/event-stream", "text/*":
			streamOK = true
		case "*/*":
			jsonOK = true
			streamOK = true
		}
	}

	if req.Method == http.MethodGet {
		if !streamOK {
			http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest)
			return
		}
	} else if (!jsonOK || !streamOK) && req.Method != http.MethodDelete { // TODO: consolidate with handling of http method below.
		http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest)
		return
	}

	sessionID := req.Header.Get(sessionIDHeader)
	var sessInfo *sessionInfo
	if sessionID != "" {
		h.mu.Lock()
		sessInfo = h.sessions[sessionID]
		h.mu.Unlock()
		if sessInfo == nil && !h.opts.Stateless {
			// Unless we're in 'stateless' mode, which doesn't perform any Session-ID
			// validation, we require that the session ID matches a known session.
			//
			// In stateless mode, a temporary transport is be created below.
			http.Error(w, "session not found", http.StatusNotFound)
			return
		}
		// Prevent session hijacking: if the session was created with a user ID,
		// verify that subsequent requests come from the same user.
		if sessInfo != nil && sessInfo.userID != "" {
			tokenInfo := auth.TokenInfoFromContext(req.Context())
			if tokenInfo == nil || tokenInfo.UserID != sessInfo.userID {
				http.Error(w, "session user mismatch", http.StatusForbidden)
				return
			}
		}
	}

	if req.Method == http.MethodDelete {
		if sessionID == "" {
			http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest)
			return
		}
		if sessInfo != nil { // sessInfo may be nil in stateless mode
			// Closing the session also removes it from h.sessions, due to the
			// onClose callback.
			sessInfo.session.Close()
		}
		w.WriteHeader(http.StatusNoContent)
		return
	}

	switch req.Method {
	case http.MethodPost, http.MethodGet:
		if req.Method == http.MethodGet && (h.opts.Stateless || sessionID == "") {
			if h.opts.Stateless {
				// Per MCP spec: server MUST return 405 if it doesn't offer SSE stream.
				// In stateless mode, GET (SSE streaming) is not supported.
				// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
				w.Header().Set("Allow", "POST")
				http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
			} else {
				// In stateful mode, GET is supported but requires a session ID.
				// This is a precondition error, similar to DELETE without session.
				http.Error(w, "Bad Request: GET requires an Mcp-Session-Id header", http.StatusBadRequest)
			}
			return
		}
	default:
		// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
		if h.opts.Stateless {
			w.Header().Set("Allow", "POST")
		} else {
			w.Header().Set("Allow", "GET, POST, DELETE")
		}
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	// [§2.7] of the spec (2025-06-18) states:
	//
	// "If using HTTP, the client MUST include the MCP-Protocol-Version:
	// <protocol-version> HTTP header on all subsequent requests to the MCP
	// server, allowing the MCP server to respond based on the MCP protocol
	// version.
	//
	// For example: MCP-Protocol-Version: 2025-06-18
	// The protocol version sent by the client SHOULD be the one negotiated during
	// initialization.
	//
	// For backwards compatibility, if the server does not receive an
	// MCP-Protocol-Version header, and has no other way to identify the version -
	// for example, by relying on the protocol version negotiated during
	// initialization - the server SHOULD assume protocol version 2025-03-26.
	//
	// If the server receives a request with an invalid or unsupported
	// MCP-Protocol-Version, it MUST respond with 400 Bad Request."
	//
	// Since this wasn't present in the 2025-03-26 version of the spec, this
	// effectively means:
	//  1. IF the client provides a version header, it must be a supported
	//     version.
	//  2. In stateless mode, where we've lost the state of the initialize
	//     request, we assume that whatever the client tells us is the truth (or
	//     assume 2025-03-26 if the client doesn't say anything).
	//
	// This logic matches the typescript SDK.
	//
	// [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header
	protocolVersion := req.Header.Get(protocolVersionHeader)
	if protocolVersion == "" {
		protocolVersion = protocolVersion20250326
	}
	if !slices.Contains(supportedProtocolVersions, protocolVersion) {
		http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest)
		return
	}

	if sessInfo == nil {
		server := h.getServer(req)
		if server == nil {
			// The getServer argument to NewStreamableHTTPHandler returned nil.
			http.Error(w, "no server available", http.StatusBadRequest)
			return
		}
		if sessionID == "" {
			// In stateless mode, sessionID may be nonempty even if there's no
			// existing transport.
			sessionID = server.opts.GetSessionID()
		}
		transport := &StreamableServerTransport{
			SessionID:    sessionID,
			Stateless:    h.opts.Stateless,
			EventStore:   h.opts.EventStore,
			jsonResponse: h.opts.JSONResponse,
			logger:       h.opts.Logger,
		}

		// Sessions without a session ID are also stateless: there's no way to
		// address them.
		stateless := h.opts.Stateless || sessionID == ""
		// To support stateless mode, we initialize the session with a default
		// state, so that it doesn't reject subsequent requests.
		var connectOpts *ServerSessionOptions
		if stateless {
			// Peek at the body to see if it is initialize or initialized.
			// We want those to be handled as usual.
			var hasInitialize, hasInitialized bool
			{
				// TODO: verify that this allows protocol version negotiation for
				// stateless servers.
				body, err := io.ReadAll(req.Body)
				if err != nil {
					http.Error(w, "failed to read body", http.StatusInternalServerError)
					return
				}
				req.Body.Close()

				// Reset the body so that it can be read later.
				req.Body = io.NopCloser(bytes.NewBuffer(body))

				msgs, _, err := readBatch(body)
				if err == nil {
					for _, msg := range msgs {
						if req, ok := msg.(*jsonrpc.Request); ok {
							switch req.Method {
							case methodInitialize:
								hasInitialize = true
							case notificationInitialized:
								hasInitialized = true
							}
						}
					}
				}
			}

			// If we don't have InitializeParams or InitializedParams in the request,
			// set the initial state to a default value.
			state := new(ServerSessionState)
			if !hasInitialize {
				state.InitializeParams = &InitializeParams{
					ProtocolVersion: protocolVersion,
				}
			}
			if !hasInitialized {
				state.InitializedParams = new(InitializedParams)
			}
			state.LogLevel = "info"
			connectOpts = &ServerSessionOptions{
				State: state,
			}
		} else {
			// Cleanup is only required in stateful mode, as transportation is
			// not stored in the map otherwise.
			connectOpts = &ServerSessionOptions{
				onClose: func() {
					h.mu.Lock()
					defer h.mu.Unlock()
					if info, ok := h.sessions[transport.SessionID]; ok {
						info.stopTimer()
						delete(h.sessions, transport.SessionID)
						if h.onTransportDeletion != nil {
							h.onTransportDeletion(transport.SessionID)
						}
					}
				},
			}
		}

		// Pass req.Context() here, to allow middleware to add context values.
		// The context is detached in the jsonrpc2 library when handling the
		// long-running stream.
		session, err := server.Connect(req.Context(), transport, connectOpts)
		if err != nil {
			http.Error(w, "failed connection", http.StatusInternalServerError)
			return
		}
		// Capture the user ID from the token info to enable session hijacking
		// prevention on subsequent requests.
		var userID string
		if tokenInfo := auth.TokenInfoFromContext(req.Context()); tokenInfo != nil {
			userID = tokenInfo.UserID
		}
		sessInfo = &sessionInfo{
			session:   session,
			transport: transport,
			userID:    userID,
		}

		if stateless {
			// Stateless mode: close the session when the request exits.
			defer session.Close() // close the fake session after handling the request
		} else {
			// Otherwise, save the transport so that it can be reused

			// Clean up the session when it times out.
			//
			// Note that the timer here may fire multiple times, but
			// sessInfo.session.Close is idempotent.
			if h.opts.SessionTimeout > 0 {
				sessInfo.timeout = h.opts.SessionTimeout
				sessInfo.timer = time.AfterFunc(sessInfo.timeout, func() {
					sessInfo.session.Close()
				})
			}
			h.mu.Lock()
			h.sessions[transport.SessionID] = sessInfo
			h.mu.Unlock()
			defer func() {
				// If initialization failed, clean up the session (#578).
				if session.InitializeParams() == nil {
					// Initialization failed.
					session.Close()
				}
			}()
		}
	}

	if req.Method == http.MethodPost {
		sessInfo.startPOST()
		defer sessInfo.endPOST()
	}

	sessInfo.transport.ServeHTTP(w, req)
}
```

### Cross-File Context

[IsLoopback — function — internal/util/net.go:1-26]
```go
func IsLoopback(addr string) bool {
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		// If SplitHostPort fails, it might be just a host without a port.
		host = strings.Trim(addr, "[]")
	}
	if host == "localhost" {
		return true
	}
	ip, err := netip.ParseAddr(host)
	if err != nil {
		return false
	}
	return ip.IsLoopback()
}
```

[disablelocalhostprotection — var — mcp/streamable.go:228]
var disablelocalhostprotection = mcpgodebug.Value("disablelocalhostprotection")

[DisableLocalhostProtection — field — mcp/streamable.go:176]
DisableLocalhostProtection bool

Control flow: The ServeHTTP function processes incoming HTTP requests through a multi-stage pipeline. First, DNS rebinding protection checks if the request arrives on a loopback interface and validates that the Host header also resolves to a loopback address, returning 403 if mismatched. Next, Accept header parsing determines which content types the client supports. Method-specific validation then enforces Accept header requirements: GET requires text/event-stream, and non-GET/non-DELETE methods require both application/json and text/event-stream. Session ID handling looks up existing sessions under mutex protection and validates user ID consistency for session hijacking prevention. DELETE requests clean up sessions. POST and GET proceed to protocol version validation, followed by session creation if needed (with special stateless mode handling that peeks at the request body). Finally, the transport's ServeHTTP is called to handle the request. Early returns occur on 403 Forbidden, 400 Bad Request, 404 Not Found, and 405 Method Not Allowed responses throughout the pipeline.

Data flow: The local address is extracted from the request context via req.Context().Value(http.LocalAddrContextKey) as a net.Addr, then converted to string via localAddr.String() and passed to util.IsLoopback(). The Host header value from req.Host is passed directly to util.IsLoopback(). Both results feed into the DNS rebinding guard condition. The Accept header values are parsed by splitting on commas and trimming whitespace, setting jsonOK and streamOK boolean flags. Session ID flows from req.Header.Get(sessionIDHeader) through mutex-protected map lookup in h.sessions. The token info flows from auth.TokenInfoFromContext(req.Context()) and is compared against sessInfo.userID for session hijacking prevention. Protocol version flows from req.Header.Get(protocolVersionHeader) with a default fallback, validated against supportedProtocolVersions via slices.Contains.

Verdict: Not Vulnerable

Reviewed region:
[mcp/streamable.go]
```go
func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// DNS rebinding protection: auto-enabled for localhost servers.
	// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
	if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" {
		if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil {
			if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) {
				http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden)
				return
			}
		}
	}

	// Allow multiple 'Accept' headers.
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax
	accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",")
	var jsonOK, streamOK bool
	for _, c := range accept {
		switch strings.TrimSpace(c) {
		case "application/json", "application/*":
			jsonOK = true
		case "text/event-stream", "text/*":
			streamOK = true
		case "*/*":
			jsonOK = true
			streamOK = true
		}
	}

	if req.Method == http.MethodGet {
		if !streamOK {
			http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest)
			return
		}
	} else if (!jsonOK || !streamOK) && req.Method != http.MethodDelete { // TODO: consolidate with handling of http method below.
		http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest)
		return
	}

	sessionID := req.Header.Get(sessionIDHeader)
	var sessInfo *sessionInfo
	if sessionID != "" {
		h.mu.Lock()
		sessInfo = h.sessions[sessionID]
		h.mu.Unlock()
		if sessInfo == nil && !h.opts.Stateless {
			// Unless we're in 'stateless' mode, which doesn't perform any Session-ID
			// validation, we require that the session ID matches a known session.
			//
			// In stateless mode, a temporary transport is be created below.
			http.Error(w, "session not found", http.StatusNotFound)
			return
		}
		// Prevent session hijacking: if the session was created with a user ID,
		// verify that subsequent requests come from the same user.
		if sessInfo != nil && sessInfo.userID != "" {
			tokenInfo := auth.TokenInfoFromContext(req.Context())
			if tokenInfo == nil || tokenInfo.UserID != sessInfo.userID {
				http.Error(w, "session user mismatch", http.StatusForbidden)
				return
			}
		}
	}

	if req.Method == http.MethodDelete {
		if sessionID == "" {
			http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest)
			return
		}
		if sessInfo != nil { // sessInfo may be nil in stateless mode
			// Closing the session also removes it from h.sessions, due to the
			// onClose callback.
			sessInfo.session.Close()
		}
		w.WriteHeader(http.StatusNoContent)
		return
	}

	switch req.Method {
	case http.MethodPost, http.MethodGet:
		if req.Method == http.MethodGet && (h.opts.Stateless || sessionID == "") {
			if h.opts.Stateless {
				// Per MCP spec: server MUST return 405 if it doesn't offer SSE stream.
				// In stateless mode, GET (SSE streaming) is not supported.
				// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
				w.Header().Set("Allow", "POST")
				http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
			} else {
				// In stateful mode, GET is supported but requires a session ID.
				// This is a precondition error, similar to DELETE without session.
				http.Error(w, "Bad Request: GET requires an Mcp-Session-Id header", http.StatusBadRequest)
			}
			return
		}
	default:
		// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
		if h.opts.Stateless {
			w.Header().Set("Allow", "POST")
		} else {
			w.Header().Set("Allow", "GET, POST, DELETE")
		}
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	// [§2.7] of the spec (2025-06-18) states:
	//
	// "If using HTTP, the client MUST include the MCP-Protocol-Version:
	// <protocol-version> HTTP header on all subsequent requests to the MCP
	// server, allowing the MCP server to respond based on the MCP protocol
	// version.
	//
	// For example: MCP-Protocol-Version: 2025-06-18
	// The protocol version sent by the client SHOULD be the one negotiated during
	// initialization.
	//
	// For backwards compatibility, if the server does not receive an
	// MCP-Protocol-Version header, and has no other way to identify the version -
	// for example, by relying on the protocol version negotiated during
	// initialization - the server SHOULD assume protocol version 2025-03-26.
	//
	// If the server receives a request with an invalid or unsupported
	// MCP-Protocol-Version, it MUST respond with 400 Bad Request."
	//
	// Since this wasn't present in the 2025-03-26 version of the spec, this
	// effectively means:
	//  1. IF the client provides a version header, it must be a supported
	//     version.
	//  2. In stateless mode, where we've lost the state of the initialize
	//     request, we assume that whatever the client tells us is the truth (or
	//     assume 2025-03-26 if the client doesn't say anything).
	//
	// This logic matches the typescript SDK.
	//
	// [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header
	protocolVersion := req.Header.Get(protocolVersionHeader)
	if protocolVersion == "" {
		protocolVersion = protocolVersion20250326
	}
	if !slices.Contains(supportedProtocolVersions, protocolVersion) {
		http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest)
		return
	}

	if sessInfo == nil {
		server := h.getServer(req)
		if server == nil {
			// The getServer argument to NewStreamableHTTPHandler returned nil.
			http.Error(w, "no server available", http.StatusBadRequest)
			return
		}
		if sessionID == "" {
			// In stateless mode, sessionID may be nonempty even if there's no
			// existing transport.
			sessionID = server.opts.GetSessionID()
		}
		transport := &StreamableServerTransport{
			SessionID:    sessionID,
			Stateless:    h.opts.Stateless,
			EventStore:   h.opts.EventStore,
			jsonResponse: h.opts.JSONResponse,
			logger:       h.opts.Logger,
		}

		// Sessions without a session ID are also stateless: there's no way to
		// address them.
		stateless := h.opts.Stateless || sessionID == ""
		// To support stateless mode, we initialize the session with a default
		// state, so that it doesn't reject subsequent requests.
		var connectOpts *ServerSessionOptions
		if stateless {
			// Peek at the body to see if it is initialize or initialized.
			// We want those to be handled as usual.
			var hasInitialize, hasInitialized bool
			{
				// TODO: verify that this allows protocol version negotiation for
				// stateless servers.
				body, err := io.ReadAll(req.Body)
				if err != nil {
					http.Error(w, "failed to read body", http.StatusInternalServerError)
					return
				}
				req.Body.Close()

				// Reset the body so that it can be read later.
				req.Body = io.NopCloser(bytes.NewBuffer(body))

				msgs, _, err := readBatch(body)
				if err == nil {
					for _, msg := range msgs {
						if req, ok := msg.(*jsonrpc.Request); ok {
							switch req.Method {
							case methodInitialize:
								hasInitialize = true
							case notificationInitialized:
								hasInitialized = true
							}
						}
					}
				}
			}

			// If we don't have InitializeParams or InitializedParams in the request,
			// set the initial state to a default value.
			state := new(ServerSessionState)
			if !hasInitialize {
				state.InitializeParams = &InitializeParams{
					ProtocolVersion: protocolVersion,
				}
			}
			if !hasInitialized {
				state.InitializedParams = new(InitializedParams)
			}
			state.LogLevel = "info"
			connectOpts = &ServerSessionOptions{
				State: state,
			}
		} else {
			// Cleanup is only required in stateful mode, as transportation is
			// not stored in the map otherwise.
			connectOpts = &ServerSessionOptions{
				onClose: func() {
					h.mu.Lock()
					defer h.mu.Unlock()
					if info, ok := h.sessions[transport.SessionID]; ok {
						info.stopTimer()
						delete(h.sessions, transport.SessionID)
						if h.onTransportDeletion != nil {
							h.onTransportDeletion(transport.SessionID)
						}
					}
				},
			}
		}

		// Pass req.Context() here, to allow middleware to add context values.
		// The context is detached in the jsonrpc2 library when handling the
		// long-running stream.
		session, err := server.Connect(req.Context(), transport, connectOpts)
		if err != nil {
			http.Error(w, "failed connection", http.StatusInternalServerError)
			return
		}
		// Capture the user ID from the token info to enable session hijacking
		// prevention on subsequent requests.
		var userID string
		if tokenInfo := auth.TokenInfoFromContext(req.Context()); tokenInfo != nil {
			userID = tokenInfo.UserID
		}
		sessInfo = &sessionInfo{
			session:   session,
			transport: transport,
			userID:    userID,
		}

		if stateless {
			// Stateless mode: close the session when the request exits.
			defer session.Close() // close the fake session after handling the request
		} else {
			// Otherwise, save the transport so that it can be reused

			// Clean up the session when it times out.
			//
			// Note that the timer here may fire multiple times, but
			// sessInfo.session.Close is idempotent.
			if h.opts.SessionTimeout > 0 {
				sessInfo.timeout = h.opts.SessionTimeout
				sessInfo.timer = time.AfterFunc(sessInfo.timeout, func() {
					sessInfo.session.Close()
				})
			}
			h.mu.Lock()
			h.sessions[transport.SessionID] = sessInfo
			h.mu.Unlock()
			defer func() {
				// If initialization failed, clean up the session (#578).
				if session.InitializeParams() == nil {
					// Initialization failed.
					session.Close()
				}
			}()
		}
	}

	if req.Method == http.MethodPost {
		sessInfo.startPOST()
		defer sessInfo.endPOST()
	}

	sessInfo.transport.ServeHTTP(w, req)
}
```

Why it is not vulnerable: The DNS rebinding protection is correctly implemented. When the local address resolves to a loopback interface (util.IsLoopback(localAddr.String()) returns true), the code enforces that the Host header must also resolve to a loopback address (util.IsLoopback(req.Host) must return true). If the Host header does not resolve to loopback, the request is rejected with a 403 Forbidden response. The util.IsLoopback function properly handles multiple address formats: it uses net.SplitHostPort to extract the host component (handling addresses with ports), trims IPv6 brackets, checks for the 'localhost' literal, and uses netip.ParseAddr with ip.IsLoopback() for IP address validation. The protection is properly gated behind DisableLocalhostProtection and disablelocalhostprotection configuration flags, allowing administrators to explicitly disable it when needed (e.g., behind a reverse proxy). Invalid or unparseable addresses in util.IsLoopback return false, which is a safe default.

Security invariants:
- When localhost protection is not explicitly disabled (via h.opts.DisableLocalhostProtection or disablelocalhostprotection), the DNS rebinding guard must execute. Enforced by: if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1"
- The local address must be successfully extracted from the request context and must be non-nil before applying the loopback check. Enforced by: if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil
- When the server is bound to a loopback interface, the Host header must also resolve to a loopback address. Enforced by: if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) { return 403 }
- util.IsLoopback must correctly parse addresses with and without port numbers. Enforced by: net.SplitHostPort(addr) to extract host, with fallback handling when it fails
- util.IsLoopback must correctly identify loopback addresses in multiple formats: 'localhost' literal, IPv4 loopback (127.0.0.0/8), and IPv6 loopback (::1). Enforced by: host == "localhost" check and ip.IsLoopback() after netip.ParseAddr
- Invalid or unparseable addresses must not be treated as loopback (safe default). Enforced by: if err != nil { return false } in util.IsLoopback
- Session access is properly synchronized to prevent race conditions. Enforced by: h.mu.Lock()/h.mu.Unlock() around h.sessions map access
- Session hijacking is prevented by verifying user ID consistency. Enforced by: if tokenInfo == nil || tokenInfo.UserID != sessInfo.userID { return 403 }

Invariant verification:
- DNS rebinding protection is enabled by default and can only be explicitly disabled: holds=true. Evidence: The condition !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" requires both the config field and the debug variable to explicitly disable the protection
- Local address extraction is safe and guards against missing context value: holds=true. Evidence: The type assertion with ok check: req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil ensures the code only proceeds when a valid address is present
- Loopback detection correctly handles host:port format addresses: holds=true. Evidence: util.IsLoopback calls net.SplitHostPort(addr) which correctly separates host and port components, then operates on the host portion only
- Loopback detection correctly identifies localhost, IPv4 loopback, and IPv6 loopback: holds=true. Evidence: util.IsLoopback checks host == "localhost" for the literal, and uses netip.ParseAddr(host) followed by ip.IsLoopback() which covers 127.0.0.0/8 and ::1
- Unparseable addresses default to non-loopback (fail-safe): holds=true. Evidence: Both error paths in util.IsLoopback (net.SplitHostPort error and netip.ParseAddr error) lead to return false or fall through to return false
- Mismatched Host header on loopback interface results in request rejection: holds=true. Evidence: if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) { http.Error(w, ..., http.StatusForbidden); return } rejects requests where local address is loopback but Host is not
- Session map access is protected by mutex: holds=true. Evidence: h.mu.Lock() before sessInfo = h.sessions[sessionID] and h.mu.Unlock() after; same pattern for write operations with h.sessions[transport.SessionID] = sessInfo

Residual risk notes: The DNS rebinding protection is skipped if the local address cannot be extracted from the request context (e.g., if the HTTP server does not populate http.LocalAddrContextKey). This is acceptable because the protection specifically applies to servers bound to loopback interfaces; if no local address information is available, the condition cannot be evaluated. The util.IsLoopback function does not perform DNS resolution on hostnames in the Host header, but this is correct behavior since the check is designed to detect when the Host header contains a non-loopback address literal or hostname that was not validated as loopback during the connection setup. The req.Host field is controlled by the client, but the IsLoopback check on it is the appropriate defense mechanism.

Yes. This code presents a plausible real vulnerability: **unbounded memory consumption from reading the entire request body into memory**, leading to potential **denial of service**.

## Observed evidence

In the `ServeHTTP` path, when `sessInfo == nil`, the handler may create a new session/transport. In the branch where the request is treated as `stateless`, it explicitly reads the full body:

```go
if stateless {
	// Peek at the body to see if it is initialize or initialized.
	var hasInitialize, hasInitialized bool
	{
		body, err := io.ReadAll(req.Body)
		if err != nil {
			http.Error(w, "failed to read body", http.StatusInternalServerError)
			return
		}
		req.Body.Close()

		// Reset the body so that it can be read later.
		req.Body = io.NopCloser(bytes.NewBuffer(body))

		msgs, _, err := readBatch(body)
		...
	}
	...
}
```

There is **no size limit** enforced before `io.ReadAll(req.Body)`.

## Reachability and attacker influence

An attacker can influence `req.Body` directly via an HTTP request.

This branch is reachable whenever:

- `sessInfo == nil`, and
- `stateless := h.opts.Stateless || sessionID == ""` evaluates true.

That includes at least:

- **stateless mode**, for ordinary POST requests with no existing session, and
- any case where a new transport is created and the resulting session remains stateless.

The code does not require authentication before this read. The Host-header protection and Accept-header validation do not constrain body size and do not mitigate this issue.

## Why this is security-relevant

`io.ReadAll` allocates enough memory to hold the entire request body. A client can send a very large body, causing:

- excessive heap growth,
- GC pressure,
- process slowdown,
- possible OOM termination.

Because the code then also wraps the same bytes in `bytes.NewBuffer(body)`, the body remains retained in memory for later processing.

This is a classic server-side resource exhaustion pattern.

## Exploitability

Realistic attack conditions are straightforward:

- the handler is exposed over HTTP to an attacker, directly or indirectly;
- the attacker can send a large POST body to a path served by this handler;
- no upstream reverse proxy/body limit blocks it first.

If the service is internet-accessible, this is remotely triggerable.
If it is localhost-only, impact is limited to local attackers or browser-based abuse scenarios, but the bug still exists in the handler.

## What does *not* look vulnerable here

The suspected localhost/DNS-rebinding protection does not itself appear obviously broken from the supplied code:

```go
if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) {
	http.Error(w, ...)
	return
}
```

`util.IsLoopback` only treats literal loopback IPs and `localhost` as loopback, which is consistent with the stated protection goal. From the supplied code alone, I do not see a concrete bypass sufficient to call that a real vulnerability.

## Root cause

The root cause is:

- **reading untrusted request bodies fully into memory without any bound**,
- for protocol-inspection convenience.

## Likely classification

- **CWE-400: Uncontrolled Resource Consumption**
- More specifically, memory exhaustion / application-level DoS.

## Verdict

**Vulnerable**: the unbounded `io.ReadAll(req.Body)` in the stateless/session-initialization path is a real denial-of-service risk.

A typical fix would be to apply a strict body limit before reading, e.g. `http.MaxBytesReader` or `io.LimitReader`, and reject oversized requests.

mcp/streamable.go: StreamableHTTPHandler.ServeHTTP, especially the sessInfo == nil path and the stateless request-body inspection block; cross-file review of internal/util/net.go:IsLoopback for the localhost protection gate.

In ServeHTTP, inside `if sessInfo == nil { ... }`, when `stateless := h.opts.Stateless || sessionID == ""` is true, the code executes `body, err := io.ReadAll(req.Body)` to inspect whether the request contains initialize/initialized messages, with no apparent size bound before reading.

The handler reads attacker-controlled HTTP request bodies fully into memory with `io.ReadAll(req.Body)` before parsing, and no request-size limit is enforced in the shown code path. This creates an unbounded memory allocation path for unauthenticated input.

A remote client that can reach this handler can send an oversized POST body and force the server to allocate memory proportional to the body size. Exploitability is straightforward if there is no upstream proxy or server-level body-size cap. The Accept-header checks, method checks, protocol-version checks, and localhost Host validation do not limit body size and do not prevent this path.

The attacker can send HTTP requests to this handler.

The request reaches the `sessInfo == nil` path.

The request is treated as stateless, which occurs when `h.opts.Stateless` is true or `sessionID == ""`.

No external request-body limit blocks the oversized request before this handler processes it.

Memory exhaustion leading to process slowdown, garbage-collection pressure, degraded availability, or process termination (DoS/OOM). Because the entire body is retained and then wrapped again for later processing (`req.Body = io.NopCloser(bytes.NewBuffer(body))`), large requests can consume substantial memory.

The localhost/DNS-rebinding protection shown at the start of ServeHTTP does not itself appear vulnerable based on the supplied code. It checks whether the server's local listening address is loopback and rejects non-loopback Host headers in that case. `util.IsLoopback` only recognizes `localhost` and literal loopback IPs, which is consistent with the intended protection. The identified issue is instead the unbounded body read in the stateless session-creation path.

If localhost protection is enabled and the listener address is loopback, requests with a non-loopback Host header are rejected.

GET requests require `Accept: text/event-stream`.

POST requests require both JSON and SSE-compatible Accept values.

Unsupported protocol versions are rejected.

Stateful unknown session IDs are rejected, and existing sessions can be bound to a user ID to prevent cross-user reuse.

These guards are explicitly implemented before deeper request handling.

None of these guards impose a maximum request-body size before the `io.ReadAll(req.Body)` call.

The vulnerable body read occurs in the new-session/stateless branch after header validation but before any visible size enforcement.

The actual exploit impact may be reduced if the surrounding HTTP server, reverse proxy, or middleware already enforces a strict body limit, but no such limit is shown in the supplied code.

The severity is availability-focused rather than confidentiality/integrity-focused.

No concrete bypass of the loopback Host-header protection is established from the supplied code.

ServeHTTP (mcp/streamable.go)
http.LocalAddrContextKey (net/http)
util.IsLoopback (internal/util/net.go)

[{'id': 'CWE-400', 'name': 'Uncontrolled Resource Consumption'}]


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/document/document.php
Function: document_listing_display

Call path: document.php (main/document/document.php) → DocumentManager::get_document_data_by_id (main/document/document.php) → document_listing_display (main/document/document.php)

### Primary Function

```php
<?php
// Document title with link and comment
$titleWithLink = $link.$session_img.'<br />'.$invisibility_span_open;
$commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
if (!empty($commentText)) {
    $titleWithLink .= '<em>'.$commentText.'</em>';
}
$titleWithLink .= $invisibility_span_close.$user_link;
$row[] = $titleWithLink;
```

### Cross-File Context

[Security::remove_XSS — method — main/inc/lib/security.lib.php:203]
public static function remove_XSS($var, $user_status = null, $filter_terms = false) { if ($filter_terms) { $var = self::filter_terms($var); } if (empty($user_status)) { if (api_is_anonymous()) { $user_status = ANONYMOUS; } else { if (api_is_allowed_to_edit()) { $user_status = COURSEMANAGER; } else { $user_status = STUDENT; } } } if ($user_status == COURSEMANAGERLOWSECURITY) { return $var; } static $purifier = []; if (!isset($purifier[$user_status])) { $cache_dir = api_get_path(SYS_ARCHIVE_PATH).'Serializer'; if (!file_exists($cache_dir)) { $mode = api_get_permissions_for_new_directories(); mkdir($cache_dir, $mode); } $config = HTMLPurifier_Config::createDefault(); $config->set('Cache.SerializerPath', $cache_dir); $config->set('Core.Encoding', api_get_system_encoding()); $config->set('HTML.Doctype', 'XHTML 1.0 Transitional'); $config->set('HTML.MaxImgLength', '2560'); $config->set('HTML.TidyLevel', 'light'); $config->set('Core.ConvertDocumentToFragment', false); $config->set('Core.RemoveProcessingInstructions', true); if (api_get_setting('enable_iframe_inclusion') == 'true') { $config->set('Filter.Custom', [new AllowIframes()]); } $config->set('Attr.AllowedFrameTargets', ['_blank', '_top', '_self', '_parent']); if ($user_status == STUDENT) { global $allowed_html_student; $config->set('HTML.SafeEmbed', true); $config->set('HTML.SafeObject', true); $config->set('Filter.YouTube', true); $config->set('HTML.FlashAllowFullScreen', true); $config->set('HTML.Allowed', $allowed_html_student); } elseif ($user_status == COURSEMANAGER) { global $allowed_html_teacher; $config->set('HTML.SafeEmbed', true); $config->set('HTML.SafeObject', true); $config->set('Filter.YouTube', true); $config->set('HTML.FlashAllowFullScreen', true); $config->set('HTML.Allowed', $allowed_html_teacher); } else { global $allowed_html_anonymous; $config->set('HTML.Allowed', $allowed_html_anonymous); } $config->set('Attr.EnableID', true); $config->set('CSS.AllowImportant', true); $config->set('CSS.AllowTricky', true); $config->set('CSS.Proprietary', true); $config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true, 'ftp' => true, 'nntp' => true, 'news' => true, 'data' => true]); $config->set('HTML.SafeIframe', true); $config->set('HTML.DefinitionID', 'html5-definitions'); $config->set('HTML.DefinitionRev', 1); if ($def = $config->maybeGetRawHTMLDefinition()) { $def->addElement('video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', ['src' => 'URI', 'type' => 'Text', 'width' => 'Length', 'height' => 'Length', 'poster' => 'URI', 'preload' => 'Enum#auto,metadata,none', 'controls' => 'Bool']); $def->addElement('audio', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', ['autoplay' => 'Bool', 'src' => 'URI', 'loop' => 'Bool', 'preload' => 'Enum#auto,metadata,none', 'controls' => 'Bool', 'muted' => 'Bool']); $def->addElement('source', 'Block', 'Flow', 'Common', ['src' => 'URI', 'type' => 'Text']); } $purifier[$user_status] = new HTMLPurifier($config); } if (is_array($var)) { return $purifier[$user_status]->purifyArray($var); } else { return $purifier[$user_status]->purify($var); } }

Control flow: User requests document listing -> document.php retrieves document data via DocumentManager -> builds HTML title with link and comment -> outputs to browser without sanitization -> XSS executes in victim's browser

Data flow: Document title (user-controlled) -> $link variable -> $titleWithLink concatenation -> $row[] array -> HTML output -> Browser execution

Verdict: Vulnerable
Vulnerable region:
[main/document/document.php]
```php
);

            // Document title with link and comment
            $titleWithLink = $link.$session_img.'<br />'.$invisibility_span_open;
            $commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
            if (!empty($commentText)) {
                $titleWithLink .= '<em>'.$commentText.'</em>';
            }
```
CWE: CWE-79
Severity: MEDIUM

Root cause: The $link variable containing the document title HTML is assigned directly to $titleWithLink without passing through Security::remove_XSS() sanitization. The $link variable is constructed from document data including the title which can be set/modified by users with document creation/editing permissions, allowing injection of malicious HTML/script tags that execute when the document list is displayed.

Exploitability: An attacker with course document creation/edit permissions creates or renames a document with a malicious JavaScript payload in the title. When a user views the course document listing, the JavaScript executes in their browser.

Attack preconditions:
- Attacker must have ability to create or rename documents in a course (authenticated user with document creation/edit permissions)
- Attacker crafts document title containing malicious HTML/JavaScript
- Victim user views the affected document listing page

Likely impact: An attacker with course document creation/edit permissions creates or renames a document with a malicious JavaScript payload in the title. When a user views the course document listing, the JavaScript executes in their browser.

);

            // Document title with link and comment
            $titleWithLink = $link.$session_img.'
'.$invisibility_span_open;
            $commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
            if (!empty($commentText)) {
                $titleWithLink .= ''.$commentText.'';
            }

**Verdict: no real vulnerability is shown in this snippet.**

### Observed code behavior

The security-relevant input here is:

```php
$document_data['comment']
```

It is processed as:

```php
$commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
```

and then inserted into HTML:

```php
if (!empty($commentText)) {
    $titleWithLink .= '<em>'.$commentText.'</em>';
}
```

### Why this is not XSS

`htmlspecialchars(..., ENT_QUOTES, $charset)` converts HTML-significant characters in the comment, including:

- `<` → `<`
- `>` → `>`
- `"` → `"`
- `'` → `'`

So even if an attacker controls `comment`, payloads like:

```html
<script>alert(1)</script>
```

or

```html
"><img src=x onerror=alert(1)>
```

would be rendered as text, not executed.

After that, `nl2br()` only adds `<br />` tags for line breaks. It does **not** reintroduce attacker-controlled HTML.

### Data flow conclusion

- **Source:** `$document_data['comment']` may be user-controlled.
- **Transformation:** `htmlspecialchars(..., ENT_QUOTES, $charset)` safely encodes it for HTML body context.
- **Sink:** concatenation into `$titleWithLink` inside `<em>...</em>`.

Given that encoding is applied immediately before output, this specific path is properly protected against reflected/stored XSS via the `comment` field.

### About the surrounding concatenation

This snippet also includes:

```php
$titleWithLink = $link.$session_img.'<br />'.$invisibility_span_open;
...
$titleWithLink .= $invisibility_span_close.$user_link;
```

Those variables are output without any escaping **in this snippet**. However, from the supplied context, there is **no evidence** about how `$link`, `$session_img`, `$invisibility_span_open`, `$invisibility_span_close`, or `$user_link` are constructed. So I cannot conclude a vulnerability from them based only on the provided code.

### Residual uncertainty

- If `$charset` were invalid/misconfigured, escaping behavior could become unreliable in some PHP configurations, but nothing in the supplied code indicates that.
- Other parts of `document_listing_display` may still be vulnerable through different variables, but this specific `comment` output is safely encoded.

## Final assessment

**No, this snippet does not present a real vulnerability.**
The attacker-influenced `comment` field is properly HTML-escaped before being inserted into the page, which prevents XSS in the shown code path.

$titleWithLink = $link.$session_img.'
'.$invisibility_span_open;
$commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
if (!empty($commentText)) {
    $titleWithLink .= ''.$commentText.'';
}
$titleWithLink .= $invisibility_span_close.$user_link;
$row[] = $titleWithLink;

The attacker-influenced value shown in this snippet, $document_data['comment'], is encoded with htmlspecialchars(..., ENT_QUOTES, $charset) before being inserted into HTML element body context. nl2br() only adds literal <br /> tags after encoding and does not restore attacker-controlled markup.

An attacker would need control over $document_data['comment'].

They would need the application to render this listing view.

No XSS impact is demonstrated for the reviewed comment output path. The comment will render as text, not executable HTML/JavaScript.

The code applies context-appropriate output encoding directly at the sink for the comment field: htmlspecialchars with ENT_QUOTES neutralizes HTML-special characters including angle brackets and quotes. The encoded result is then wrapped in <em>...</em>, and nl2br only inserts safe line-break tags. Based on the supplied code, this prevents stored/reflected XSS via the comment field in this path.

$document_data['comment'] is transformed by htmlspecialchars(..., ENT_QUOTES, $charset) before concatenation into HTML.

The comment is inserted only into HTML body content, not into an attribute, script, or URL context in the shown code.

nl2br() operates on the already-encoded string and only introduces fixed <br /> markup.

htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset) followed by nl2br(...)

User-controlled comment content is HTML-encoded before output, preventing markup/script execution in this sink.

Other concatenated variables in the same snippet ($link, $session_img, $invisibility_span_open, $invisibility_span_close, $user_link) are not escaped here, but their construction is not provided. No vulnerability can be concluded for them from the supplied evidence alone.

If $charset were invalid or misconfigured, escaping reliability could theoretically be affected, but no such condition is evidenced in the supplied 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/torvalds/linux-2.6
Language: C
File: net/ceph/auth_x.c
Function: ceph_x_decrypt

Call path: ceph_x_handle_reply (net/ceph/auth_x.c) → ceph_x_proc_ticket_reply (net/ceph/auth_x.c) → process_one_ticket (net/ceph/auth_x.c) → ceph_x_decrypt (net/ceph/auth_x.c) → ceph_decode_copy (include/linux/ceph/decode.h)

### Primary Function

```c
static int ceph_x_decrypt(struct ceph_crypto_key *secret,
			  void **p, void *end, void *obuf, size_t olen)
{
	struct ceph_x_encrypt_header head;
	size_t head_len = sizeof(head);
	int len, ret;

	len = ceph_decode_32(p);
	if (*p + len > end)
		return -EINVAL;

	dout("ceph_x_decrypt len %d\n", len);
	ret = ceph_decrypt2(secret, &head, &head_len, obuf, &olen,
			    *p, len);
	if (ret)
		return ret;
	if (head.struct_v != 1 || le64_to_cpu(head.magic) != CEPHX_ENC_MAGIC)
		return -EPERM;
	*p += len;
	return olen;
}
```

### Cross-File Context

[TEMP_TICKET_BUF_LEN — macro — net/ceph/auth_x.c:16]
TEMP_TICKET_BUF_LEN → 256  (net/ceph/auth_x.c:16)

[CEPHX_ENC_MAGIC — constant — net/ceph/auth_x_protocol.h:83]
CEPHX_ENC_MAGIC → 0xff009cad8826aa55ull  (net/ceph/auth_x_protocol.h:83)

[ceph_x_encrypt_header — struct — net/ceph/auth_x_protocol.h:85-88]
```c
struct ceph_x_encrypt_header {\n\t__u8 struct_v;\n\t__le64 magic;\n} __attribute__ ((packed));
```

[ceph_decode_copy — sink — include/linux/ceph/decode.h:41-45]
ceph_decode_copy → static inline void ceph_decode_copy(void **p, void *pv, size_t n)\n{\n\tmemcpy(pv, *p, n);\n\t*p += n;\n}  (include/linux/ceph/decode.h:41-45)

[ceph_decode_32 — function — include/linux/ceph/decode.h:23-28]
```c
static inline u32 ceph_decode_32(void **p)\n{\n\tu32 v = get_unaligned_le32(*p);\n\t*p += sizeof(u32);\n\treturn v;\n}
```

[ceph_decode_8_safe — macro — include/linux/ceph/decode.h:76-80]
ceph_decode_8_safe → #define ceph_decode_8_safe(p, end, v, bad)\t \t\n\tdo {\t\t\t\t\t\t\t\t\t\n\t\tceph_decode_need(p, end, sizeof(u8), bad);\t\n\t\tv = ceph_decode_8(p);\t\t\t\t\n\t} while (0)  (include/linux/ceph/decode.h:76-80)

[ceph_decode_32_safe — macro — include/linux/ceph/decode.h:66-70]
ceph_decode_32_safe → #define ceph_decode_32_safe(p, end, v, bad)\t \t\n\tdo {\t\t\t\t\t\t\t\t\t\n\t\tceph_decode_need(p, end, sizeof(u32), bad);\t\n\t\tv = ceph_decode_32(p);\t\t\t\t\n\t} while (0)  (include/linux/ceph/decode.h:66-70)

[ceph_decode_need — macro — include/linux/ceph/decode.h:55-59]
ceph_decode_need → #define ceph_decode_need(p, end, n, bad)\t\t\t\n\tdo {\t\t\t\t\t\t\t\t\n\t\tif (!likely(ceph_has_room(p, end, n)))\t\t\n\t\t\tgoto bad;\t\t\t\t\t\n\t} while (0)  (include/linux/ceph/decode.h:55-59)

Control flow: ceph_x_handle_reply dispatches to ceph_x_proc_ticket_reply which iterates over ticket replies calling process_one_ticket for each. process_one_ticket calls ceph_x_decrypt to decrypt ticket data. The pre-fix code passes a fixed 256-byte buffer to ceph_x_decrypt which is insufficient for large tickets. ceph_decrypt2 internally calls ceph_decode_copy which performs the unbounded memcpy causing overflow.

Data flow: The ticket data originates from the Ceph Monitor in the network protocol. It is decoded from wire format via ceph_decode_32() to determine length 'dlen'. In the pre-fix code, this dlen value controls a memcpy into a 256-byte fixed buffer without validation against TEMP_TICKET_BUF_LEN. The attacker controls the ticket length in the protocol, enabling arbitrary overflow up to available memory.

Verdict: Vulnerable
Vulnerable region:
[net/ceph/auth_x.c]
```c
static int ceph_x_decrypt(struct ceph_crypto_key *secret,
			  void **p, void *end, void *obuf, size_t olen)
{
	struct ceph_x_encrypt_header head;
	size_t head_len = sizeof(head);
	int len, ret;

	len = ceph_decode_32(p);
	if (*p + len > end)
		return -EINVAL;

	dout("ceph_x_decrypt len %d\n", len);
	ret = ceph_decrypt2(secret, &head, &head_len, obuf, &olen,
			    *p, len);
	if (ret)
		return ret;
	if (head.struct_v != 1 || le64_to_cpu(head.magic) != CEPHX_ENC_MAGIC)
		return -EPERM;
	*p += len;
	return olen;
}
```
CWE: CWE-399
Severity: HIGH

Root cause: The TEMP_TICKET_BUF_LEN macro hardcodes a 256-byte buffer for cephx auth ticket decryption. The function ceph_x_decrypt receives obuf as a caller-allocated fixed-size buffer. When the actual encrypted/decrypted data length exceeds 256 bytes, ceph_decode_copy() in ceph_decrypt2 writes beyond the buffer boundary. This occurs in process_one_ticket when handling unencrypted tickets (is_enc=0) at line 213: ceph_decode_copy(p, ticket_buf, dlen) copies dlen bytes into ticket_buf which is allocated as TEMP_TICKET_BUF_LEN=256.

Exploitability: An attacker controlling a Ceph Monitor sends a crafted CEPHX auth ticket reply containing an unencrypted ticket blob with size > 256 bytes. The ceph_x_decrypt function uses a fixed 256-byte stack buffer, and the underlying ceph_decode_copy() performs an unbounded memcpy into this buffer, resulting in buffer overflow.

Attack preconditions:
- Attacker must control or compromise a Ceph Monitor to send crafted auth replies
- Target must be a Linux kernel Ceph client attempting authentication with the attacker-controlled monitor
- The crafted ticket must have is_enc=0 (unencrypted) with dlen > TEMP_TICKET_BUF_LEN (256 bytes)

Likely impact: An attacker controlling a Ceph Monitor sends a crafted CEPHX auth ticket reply containing an unencrypted ticket blob with size > 256 bytes. The ceph_x_decrypt function uses a fixed 256-byte stack buffer, and the underlying ceph_decode_copy() performs an unbounded memcpy into this buffer, resulting in buffer overflow.

Yes — this function shows a real bounds-checking bug that can lead to an out-of-bounds read.

## Observed code behavior

```c
len = ceph_decode_32(p);
if (*p + len > end)
	return -EINVAL;
```

From context:

```c
static inline u32 ceph_decode_32(void **p)
{
	u32 v = get_unaligned_le32(*p);
	*p += sizeof(u32);
	return v;
}
```

So `ceph_x_decrypt()` does this in order:

1. Reads 4 bytes from `*p` via `get_unaligned_le32(*p)`.
2. Advances `*p` by 4.
3. Only afterward checks whether the claimed ciphertext length fits in `[p, end)`.

## The problem

There is **no check that at least 4 bytes remain** before calling `ceph_decode_32(p)`.

The available safe helpers exist in this codebase:

- `ceph_decode_need(...)`
- `ceph_decode_32_safe(...)`

But this function does not use them.

That means if `*p` is near or at `end`, `ceph_decode_32()` may read past the end of the received message buffer before any validation occurs.

## Why the later check is insufficient

This check:

```c
if (*p + len > end)
	return -EINVAL;
```

only validates the variable-length encrypted payload **after** the 4-byte length field has already been fetched.

So it protects against oversized `len`, but it does **not** protect the initial length-field read itself.

## Attacker influence

The call path is from ticket reply handling:

- `ceph_x_handle_reply`
- `ceph_x_proc_ticket_reply`
- `process_one_ticket`
- `ceph_x_decrypt`

This is parsing protocol data from a Ceph peer/server reply, so `p`/`end` are derived from network-controlled input. A malformed or truncated reply can therefore influence this condition.

## Exploitability

A malicious or compromised peer sending a truncated message could cause:

- an out-of-bounds read of up to 4 bytes in kernel memory parsing logic;
- likely a kernel crash/fault, depending on memory layout and whether the read crosses an unmapped boundary.

This is a read, not a write, so the most direct impact is denial of service. Kernel OOB reads can sometimes have broader consequences, but from the supplied code the concrete issue is an unchecked read.

## Classification

Most precise classification from the shown code:

- **Out-of-bounds read / improper bounds checking**
- CWE-125 / CWE-787 family, with CWE-125 being the best fit here

## Why this appears to be a real vulnerability

Concrete evidence:

- `ceph_decode_32()` dereferences `*p` immediately.
- `ceph_x_decrypt()` performs no `end - *p >= 4` check before calling it.
- Safe decode macros exist but are not used here.
- Input originates from reply parsing, so attacker-controlled truncation is plausible.

## Verdict

**Vulnerable.**
`ceph_x_decrypt()` can read 4 bytes past the end of the input buffer because it decodes the length field before verifying that the buffer contains at least 4 bytes.

The function reads a 32-bit length field via ceph_decode_32(p) before verifying that at least 4 bytes remain in the input buffer.

Missing bounds check before decoding the 4-byte length field. ceph_decode_32() immediately performs get_unaligned_le32(*p) and advances *p by 4, but ceph_x_decrypt() does not first ensure that [*p, end) contains sizeof(u32) bytes. The later check only validates the variable-sized ciphertext after the length field has already been read.

A malformed or truncated Ceph reply can cause the parser to read up to 4 bytes past the end of the received buffer while decoding the length field. In kernel context this is realistically exploitable for denial of service via crash/fault; broader impact is not established from the supplied code.

An attacker can cause ceph_x_decrypt() to process a truncated or malformed reply buffer.

The attacker-controlled input reaches ceph_x_handle_reply -> ceph_x_proc_ticket_reply -> process_one_ticket -> ceph_x_decrypt.

The buffer position *p is at or within fewer than 4 bytes of end when ceph_x_decrypt() is entered.

Kernel out-of-bounds read of the 32-bit length field

After the length is decoded, the function checks if (*p + len > end) and rejects oversized ciphertext regions.

After decryption, it validates head.struct_v == 1 and head.magic == CEPHX_ENC_MAGIC before accepting the plaintext.

The ciphertext-length bounds check does not protect the initial 4-byte read performed by ceph_decode_32().

Header validation occurs only after ceph_decrypt2() returns and therefore does not mitigate the pre-check out-of-bounds read.

Safe decode helpers exist in the codebase (ceph_decode_need, ceph_decode_32_safe) but are not used here.

The supplied code does not show whether callers guarantee at least 4 bytes remain before invoking ceph_x_decrypt(). Based on this function alone, that invariant is neither checked nor documented.

The expression '*p + len > end' uses void* arithmetic, which is a GNU C extension; the primary confirmed issue here is still the unchecked initial ceph_decode_32() read.

[{'id': 'CWE-125', 'name': 'Out-of-bounds Read'}, {'id': 'CWE-20', 'name': 'Improper Input Validation'}]