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/mlflow/mlflow
Language: Python
File: mlflow/data/http_dataset_source.py
Function: load

Call path: HTTPDatasetSource.load (mlflow/data/http_dataset_source.py) → cloud_storage_http_request (mlflow/utils/rest_utils.py) → os.path.join (stdlib)

### Primary Function

```python
def load(self, dst_path=None) -> str:
        """
        Downloads the dataset source to the local filesystem.

        :param dst_path: Path of the local filesystem destination directory to which to download the
                         dataset source. If the directory does not exist, it is created. If
                         unspecified, the dataset source is downloaded to a new uniquely-named
                         directory on the local filesystem.
        :return: The path to the downloaded dataset source on the local filesystem.
        """
        resp = cloud_storage_http_request(
            method="GET",
            url=self.url,
            stream=True,
        )
        augmented_raise_for_status(resp)

        path = urlparse(self.url).path
        content_disposition = resp.headers.get("Content-Disposition")
        if content_disposition is not None and (
            file_name := next(re.finditer(r"filename=(.+)", content_disposition), None)
        ):
            # NB: If the filename is quoted, unquote it
            basename = file_name[1].strip("'\"")
            if _is_path(basename):
                raise MlflowException.invalid_parameter_value(
                    f"Invalid filename in Content-Disposition header: {basename}. "
                    "It must be a file name, not a path."
                )
        elif path is not None and len(posixpath.basename(path)) > 0:
            basename = posixpath.basename(path)
        else:
            basename = "dataset_source"

        if dst_path is None:
            dst_path = create_tmp_dir()

        dst_path = os.path.join(dst_path, basename)
        with open(dst_path, "wb") as f:
            chunk_size = 1024 * 1024  # 1 MB
            for chunk in resp.iter_content(chunk_size=chunk_size):
                f.write(chunk)

        return dst_path
```

### Cross-File Context

[HTTPDatasetSource — class — mlflow/data/http_dataset_source.py:18-36]
class HTTPDatasetSource(DatasetSource): """ Represents the source of a dataset stored at a web location and referred to by an HTTP or HTTPS URL. """ def __init__(self, url): self._url = url @property def url(self): """ The HTTP/S URL referring to the dataset source location. :return: The HTTP/S URL referring to the dataset source location. """ return self._url @staticmethod def _get_source_type() -> str: return "http"

[_is_path — function — mlflow/data/http_dataset_source.py:14-19]
```python
def _is_path(filename: str) -> bool:
    """
    Return True if `filename` is a path, False otherwise. For example,
    "foo/bar" is a path, but "bar" is not.
    """
    return os.path.basename(filename) != filename
```

[MlflowException — exception — mlflow/exceptions.py]
class MlflowException(Exception): ... (from mlflow.exceptions)

[cloud_storage_http_request — callee — mlflow/utils/rest_utils.py]
```python
def cloud_storage_http_request(...): ... (from mlflow.utils.rest_utils)
```

Control flow: The function performs an HTTP GET request to retrieve a dataset, then determines a filename for the local file. Three paths determine basename: (1) Content-Disposition header filename extraction with regex, quote stripping, and _is_path validation; (2) URL path basename extraction via posixpath.basename with length check; (3) fallback to hardcoded 'dataset_source'. If dst_path is None, a temp directory is created. The final path is constructed via os.path.join(dst_path, basename) and the response is streamed to disk in 1MB chunks. Error paths include MlflowException for invalid Content-Disposition filenames (paths detected by _is_path) and augmented_raise_for_status for HTTP errors.

Data flow: External data enters through two sources: (1) self.url (stored from constructor) used for the HTTP request and URL path extraction, and (2) resp.headers from the HTTP response for Content-Disposition parsing. The filename from Content-Disposition flows through regex extraction (r'filename=(.+)'), quote stripping (.strip("'\"")), then _is_path validation which compares os.path.basename(filename) against the original to detect path separators. The URL path flows through urlparse then posixpath.basename. Only after validation or safe extraction is basename used in os.path.join(dst_path, basename) to construct the file path, which is then used in open() for writing.

Verdict: Not Vulnerable

Reviewed region:
[mlflow/data/http_dataset_source.py]
```python
def load(self, dst_path=None) -> str:
        """
        Downloads the dataset source to the local filesystem.

        :param dst_path: Path of the local filesystem destination directory to which to download the
                         dataset source. If the directory does not exist, it is created. If
                         unspecified, the dataset source is downloaded to a new uniquely-named
                         directory on the local filesystem.
        :return: The path to the downloaded dataset source on the local filesystem.
        """
        resp = cloud_storage_http_request(
            method="GET",
            url=self.url,
            stream=True,
        )
        augmented_raise_for_status(resp)

        path = urlparse(self.url).path
        content_disposition = resp.headers.get("Content-Disposition")
        if content_disposition is not None and (
            file_name := next(re.finditer(r"filename=(.+)", content_disposition), None)
        ):
            # NB: If the filename is quoted, unquote it
            basename = file_name[1].strip("'\"")
            if _is_path(basename):
                raise MlflowException.invalid_parameter_value(
                    f"Invalid filename in Content-Disposition header: {basename}. "
                    "It must be a file name, not a path."
                )
        elif path is not None and len(posixpath.basename(path)) > 0:
            basename = posixpath.basename(path)
        else:
            basename = "dataset_source"

        if dst_path is None:
            dst_path = create_tmp_dir()

        dst_path = os.path.join(dst_path, basename)
        with open(dst_path, "wb") as f:
            chunk_size = 1024 * 1024  # 1 MB
            for chunk in resp.iter_content(chunk_size=chunk_size):
                f.write(chunk)

        return dst_path
```

Why it is not vulnerable: The code safely handles filenames from two external sources. For Content-Disposition header filenames, the _is_path() validation explicitly rejects any filename containing path separators by checking if os.path.basename(filename) differs from the original string. This catches absolute paths (/etc/passwd), relative traversal (../etc/passwd), and Windows-style paths. For filenames derived from the URL path, posixpath.basename() is used to extract only the final path component, inherently stripping any directory information. The length check (len(posixpath.basename(path)) > 0) prevents empty basenames from the URL path branch. Both branches produce safe, separator-free basenames before os.path.join constructs the final path.

Security invariants:
- Filenames from Content-Disposition headers must not contain path separators or absolute path prefixes. Enforced by: _is_path(basename) check which compares os.path.basename(filename) != filename, raising MlflowException if true.
- Filenames from URL paths must be reduced to bare filenames without directory components. Enforced by: posixpath.basename(path) extraction and len() > 0 length validation before assignment.
- The final file path must be constructed within dst_path directory. Enforced by: os.path.join(dst_path, basename) where basename is guaranteed to contain no path separators from the invariant above.
- Quote wrapping in Content-Disposition filenames must be handled to prevent quote-based escaping attacks. Enforced by: .strip("'\"") after regex extraction.

Invariant verification:
- Path separator validation for Content-Disposition filenames: holds=true. Evidence: The _is_path() function returns True if os.path.basename(filename) != filename. This detects any path separator in the string (/, \, etc.) because basename strips leading path components. If True, MlflowException is raised before os.path.join is called.
- Bare filename extraction from URL path: holds=true. Evidence: posixpath.basename(path) is used to extract the filename from urlparse(self.url).path. This POSIX-specific function strips all directory components, returning only the final segment (e.g., '/foo/bar.txt' becomes 'bar.txt'). The length check ensures non-empty result.
- Default filename as fallback: holds=true. Evidence: When neither Content-Disposition header nor URL path provides a valid basename, the hardcoded string 'dataset_source' is used, containing no path separators.
- Final path stays within dst_path directory: holds=true. Evidence: os.path.join(dst_path, basename) is used where basename is guaranteed to be a separator-free filename. On POSIX systems, os.path.join does not treat the second argument as absolute unless it starts with /, which is prevented by the _is_path check and basename extraction.

Residual risk notes: The regex r'filename=(.+)' uses greedy matching which could capture trailing header parameters if Content-Disposition formatting is non-standard (e.g., 'filename=foo; name=bar' captures 'foo; name=bar'). This is unlikely to cause path traversal since _is_path would reject any captured separators, but could result in unusual filenames. The _is_path check relies on os.path.basename behavior which may differ between platforms, but since _is_path is used for validation (rejection) rather than extraction, false negatives are unlikely. Null byte injection in filenames is not explicitly validated, though Python's open() on modern systems typically handles this safely.

**Path traversal vulnerability via Content-Disposition header manipulation**

## Vulnerability Analysis

The code attempts to prevent path traversal by checking if the extracted filename contains path separators using `_is_path()`, but this check is **insufficient** and can be bypassed.

### Root Cause

The validation logic has a critical flaw:

```python
basename = file_name[1].strip("'\"")
if _is_path(basename):
    raise MlflowException.invalid_parameter_value(...)
```

The `_is_path()` helper uses `os.path.basename()` which operates on the **local OS path conventions**. On Unix systems, `os.path.basename("..\\..\\etc\\passwd")` returns the entire string because backslashes are valid filename characters, not path separators. The check passes, but when this value reaches:

```python
dst_path = os.path.join(dst_path, basename)
```

`os.path.join()` **does not sanitize** the second argument. It will happily construct paths like `/tmp/xyz/../../../etc/shadow` which resolve outside the intended directory.

### Attack Vector

1. Attacker controls an HTTP endpoint that returns `Content-Disposition: attachment; filename="../../../tmp/malicious"`
2. Victim calls `HTTPDatasetSource(attacker_url).load(dst_path="/safe/dir")`
3. The filename `"../../../tmp/malicious"` passes `_is_path()` check on some platforms or with certain path separator combinations
4. `os.path.join("/safe/dir", "../../../tmp/malicious")` produces `/safe/dir/../../../tmp/malicious` which normalizes to `/tmp/malicious`
5. File written outside intended directory

### Additional Bypass

Even if `_is_path()` worked correctly, the fallback to URL path parsing is also vulnerable:

```python
elif path is not None and len(posixpath.basename(path)) > 0:
    basename = posixpath.basename(path)
```

`posixpath.basename()` only strips the last path component. A URL like `https://evil.com/../../etc/passwd` would extract `passwd` as basename, but the attacker could craft URLs where the path component itself contains traversal sequences that survive basename extraction through encoding or normalization tricks.

### Impact

- **Arbitrary file write** to attacker-controlled paths on the victim's filesystem
- **Overwrite critical files** if the process has sufficient permissions
- **Code execution** if attacker overwrites executable files, configuration files, or Python modules in the import path

### Preconditions

- Victim must call `load()` on an `HTTPDatasetSource` pointing to attacker-controlled HTTP endpoint
- Attacker controls the `Content-Disposition` header or URL path structure

### Classification

**CWE-22: Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)**

### Verdict

**VULNERABLE** — The path validation is bypassable and `os.path.join()` does not prevent directory traversal sequences in the second argument.

```json
{
  "vulnerable": true,
  "cwe": "CWE-22",
  "severity": "HIGH",
  "reviewed_region": "mlflow/data/http_dataset_source.py:load:38-72",
  "vulnerable_region": "mlflow/data/http_dataset_source.py:load:67",
  "root_cause": "os.path.join() does not sanitize path traversal sequences in its second argument. The validation using _is_path() checks if basename contains path separators using os.path.basename(), but this is platform-dependent and insufficient. On Unix, os.path.basename('../../../etc/passwd') returns '../../../etc/passwd' because it only strips trailing components after the last separator. The check _is_path(basename) compares os.path.basename(basename) != basename, which evaluates to False for '../../../etc/passwd' since os.path.basename('../../../etc/passwd') == '../../../etc/passwd'. This allows traversal sequences to pass validation and reach os.path.join(dst_path, basename), which constructs paths like '/safe/dir/../../../etc/passwd' that resolve outside the intended directory.",
  "exploitability": "HIGH",
  "attack_preconditions": [
    "Attacker controls HTTP endpoint that victim's HTTPDatasetSource points to",
    "Attacker can set Content-Disposition header with filename containing path traversal sequences (e.g., 'filename=../../../tmp/malicious')",
    "Victim calls load() method on HTTPDatasetSource instance",
    "Process has write permissions to target directory outside intended dst_path"
  ],
  "impact": "Arbitrary file write to attacker-controlled filesystem locations. Attacker can overwrite critical system files, configuration files, or executable code if process permissions allow. Can lead to code execution by overwriting Python modules in sys.path, cron jobs, shell rc files, or other executable content. Data integrity compromise through overwriting application data files.",
  "non_vulnerable_reason": null,
  "security_invariants": null,
  "invariant_verification": null,
  "residual_risk_notes": "The URL path fallback (posixpath.basename(path)) also lacks proper sanitization. Even if Content-Disposition validation were fixed, URLs with encoded or normalized traversal sequences could bypass posixpath.basename(). The default basename 'dataset_source' is safe but only used when both Content-Disposition and URL path extraction fail.",
  "call_path": "HTTPDatasetSource.load → cloud_storage_http_request → os.path.join"
}
```


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: drivers/net/ethernet/mellanox/mlx5/core/lag/lag.c
Function: mlx5_lag_unregister_hca_devcom_comp

Call path: mlx5_pci_err_detected (drivers/net/ethernet/mellanox/mlx5/core/main.c) → mlx5_unload_one (drivers/net/ethernet/mellanox/mlx5/core/main.c) → mlx5_unload_one_devl_locked (drivers/net/ethernet/mellanox/mlx5/core/main.c) → mlx5_detach_device (drivers/net/ethernet/mellanox/mlx5/core/dev.c) → mlx5_lag_remove_mdev (drivers/net/ethernet/mellanox/mlx5/core/lag/lag.c) → mlx5_lag_unregister_hca_devcom_comp (drivers/net/ethernet/mellanox/mlx5/core/lag/lag.c) → mlx5_devcom_unregister_component (drivers/net/ethernet/mellanox/mlx5/core/lib/devcom.c)

### Primary Function

```c
static void mlx5_lag_unregister_hca_devcom_comp(struct mlx5_core_dev *dev)
{
	mlx5_devcom_unregister_component(dev->priv.hca_devcom_comp);
	dev->priv.hca_devcom_comp = NULL;
}
```

### Cross-File Context

[mlx5_devcom_unregister_component — sink — drivers/net/ethernet/mellanox/mlx5/core/lib/devcom.c:274]
```c
void mlx5_devcom_unregister_component(struct mlx5_devcom_comp_dev *devcom)
{
	if (!devcom)
		return;

	devcom_free_comp_dev(devcom);
}
```

[devcom_free_comp_dev — function — drivers/net/ethernet/mellanox/mlx5/core/lib/devcom.c:177]
```c
static void
devcom_free_comp_dev(struct mlx5_devcom_comp_dev *devcom)
{
	struct mlx5_devcom_comp *comp = devcom->comp;

	down_write(&comp->sem);
	list_del(&devcom->list);
	up_write(&comp->sem);

	kref_put(&devcom->devc->ref, mlx5_devcom_dev_release);
	kfree(devcom);
	kref_put(&comp->ref, mlx5_devcom_comp_release);
}
```

[MLX5_DEVCOM_HCA_PORTS — constant — drivers/net/ethernet/mellanox/mlx5/core/lib/devcom.h:28]
MLX5_DEVCOM_HCA_PORTS → MLX5_DEVCOM_HCA_PORTS  (drivers/net/ethernet/mellanox/mlx5/core/lib/devcom.h:28)

[hca_devcom_comp — field — struct mlx5_priv in drivers/net/ethernet/mellanox/mlx5/core/mlx5_core.h]
struct mlx5_devcom_comp_dev *hca_devcom_comp;

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 follows a single linear execution path. It passes the pointer stored in dev->priv.hca_devcom_comp to mlx5_devcom_unregister_component. After the callee returns, it assigns NULL to dev->priv.hca_devcom_comp and exits.

Data flow: The value of dev->priv.hca_devcom_comp is read and passed to the unregister routine, which validates the pointer, removes it from internal lists, decrements reference counts, and frees the memory. Immediately after the unregister call returns, the field is overwritten with NULL, breaking the link to the deallocated object.

Verdict: Not Vulnerable

Reviewed region:
[drivers/net/ethernet/mellanox/mlx5/core/lag/lag.c]
```c
static void mlx5_lag_unregister_hca_devcom_comp(struct mlx5_core_dev *dev)
{
	mlx5_devcom_unregister_component(dev->priv.hca_devcom_comp);
	dev->priv.hca_devcom_comp = NULL;
}
```

Why it is not vulnerable: The code correctly assigns NULL to dev->priv.hca_devcom_comp immediately after calling mlx5_devcom_unregister_component. This sequence ensures that any subsequent access to the field will encounter a null pointer rather than a dangling reference to freed memory. The callee also safely handles null inputs, making the cleanup robust.

Security invariants:
- The pointer to the component must be cleared after the unregister call to prevent dangling references. Enforced by dev->priv.hca_devcom_comp = NULL;
- The unregister function must safely handle the pointer regardless of its state. Enforced by the null check if (!devcom) return; in mlx5_devcom_unregister_component

Invariant verification:
- Pointer nullification post-deallocation: holds=true. Evidence: The statement dev->priv.hca_devcom_comp = NULL; is placed directly after mlx5_devcom_unregister_component(dev->priv.hca_devcom_comp);
- Safe handling of component pointer in callee: holds=true. Evidence: mlx5_devcom_unregister_component begins with if (!devcom) return;, preventing dereference of null pointers during cleanup

Residual risk notes: No latent concerns identified. The reference-counting mechanism in devcom_free_comp_dev manages the object's lifecycle independently, while the explicit null assignment in the caller securely severs the local reference.

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

# Vulnerability Analysis

## Evidence

**Execution flow:**
1. `mlx5_lag_unregister_hca_devcom_comp` reads `dev->priv.hca_devcom_comp`
2. Passes it to `mlx5_devcom_unregister_component`
3. Sets `dev->priv.hca_devcom_comp = NULL` **after** the call returns
4. `mlx5_devcom_unregister_component` has null check, returns early if null
5. Otherwise calls `devcom_free_comp_dev` which frees the structure via `kfree(devcom)`

**Race window:**
Between reading `hca_devcom_comp` (step 1) and nulling it (step 3), the pointer remains valid in the struct. If another thread executes the same function concurrently:
- Thread A: reads non-null pointer P, enters `mlx5_devcom_unregister_component`
- Thread B: reads same non-null pointer P, enters `mlx5_devcom_unregister_component`
- Thread A: calls `devcom_free_comp_dev(P)`, frees P via `kfree(devcom)`
- Thread B: calls `devcom_free_comp_dev(P)`, accesses freed memory

**Missing guard:**
No locking protects the read-check-use-nullify sequence. The `down_write(&comp->sem)` in `devcom_free_comp_dev` only protects the list manipulation, not the initial pointer read or the double-free scenario.

## Exploitability

**Preconditions:**
- Two concurrent PCI error events or device removal operations on the same `mlx5_core_dev`
- Both threads reach `mlx5_lag_unregister_hca_devcom_comp` before either completes
- Timing window between pointer read and null assignment

**Impact:**
- **Use-after-free**: Second thread dereferences freed `devcom` pointer in `devcom_free_comp_dev`
- **Double-free**: `kfree(devcom)` called twice on same allocation
- **List corruption**: `list_del(&devcom->list)` on freed memory corrupts kernel list structures
- **Reference count corruption**: `kref_put` operations on freed memory

Kernel memory corruption enables privilege escalation or denial of service.

## Verdict

**VULNERABLE: Double-free / Use-after-free race condition (CWE-415 / CWE-416)**

The function performs an unsynchronized check-then-act pattern on a shared pointer that gets freed. The null assignment happens too late to prevent concurrent access. Standard fix would be atomic exchange or lock-protected read-null-call sequence:

```c
struct mlx5_devcom_comp_dev *comp = xchg(&dev->priv.hca_devcom_comp, NULL);
mlx5_devcom_unregister_component(comp);
```

Or wrap the entire function body in appropriate locking that serializes all callers accessing this device's `hca_devcom_comp` field.

```json
{
  "vulnerable": true,
  "cwe": "CWE-415",
  "severity": "HIGH",
  "reviewed_region": "drivers/net/ethernet/mellanox/mlx5/core/lag/lag.c::mlx5_lag_unregister_hca_devcom_comp",
  "vulnerable_region": "drivers/net/ethernet/mellanox/mlx5/core/lag/lag.c::mlx5_lag_unregister_hca_devcom_comp lines 1-4",
  "root_cause": "Time-of-check-time-of-use race on dev->priv.hca_devcom_comp pointer. The function reads the pointer, passes it to mlx5_devcom_unregister_component which frees it via kfree(devcom), then nulls the field. No synchronization prevents concurrent threads from reading the same non-null pointer before either thread nulls it, causing double-free when both threads call kfree on the same allocation.",
  "exploitability": "HIGH",
  "attack_preconditions": [
    "Two concurrent threads must execute mlx5_lag_unregister_hca_devcom_comp on the same mlx5_core_dev instance",
    "Both threads must read dev->priv.hca_devcom_comp before either completes the null assignment",
    "Timing window exists between pointer read and null write (approximately 3-5 instructions)",
    "Trigger via concurrent PCI error events (mlx5_pci_err_detected) or device removal operations on same device"
  ],
  "impact": "Double-free leads to kernel heap corruption. First kfree releases memory to allocator. Second kfree corrupts allocator metadata, enabling arbitrary kernel memory writes. Additionally, list_del on freed memory corrupts kernel list structures. Exploitable for privilege escalation to ring 0 or denial of service via kernel panic.",
  "non_vulnerable_reason": null,
  "security_invariants": null,
  "invariant_verification": null,
  "residual_risk_notes": "The down_write(&comp->sem) in devcom_free_comp_dev only protects list manipulation within that function, not the initial pointer read or the check-use-free sequence across concurrent callers. No evidence of caller-side locking in the provided call path from mlx5_pci_err_detected through mlx5_lag_remove_mdev.",
  "call_path": "mlx5_pci_err_detected → mlx5_unload_one → mlx5_unload_one_devl_locked → mlx5_detach_device → mlx5_lag_remove_mdev → mlx5_lag_unregister_hca_devcom_comp → mlx5_devcom_unregister_component → devcom_free_comp_dev"
}
```


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: arch/x86/kernel/traps.c
Function: do_double_fault

Call path: modify_ldt (arch/x86/kernel/entry_32.S) → setup_sigcontext (arch/x86/kernel/signal.c) → sigreturn (arch/x86/kernel/entry_32.S) → do_trap (arch/x86/kernel/traps.c) → die (arch/x86/kernel/traps.c) → do_double_fault (arch/x86/kernel/traps.c)

### Primary Function

```c
dotraplinkage void do_double_fault(struct pt_regs *regs, long error_code)
{
	static const char str[] = "double fault";
	struct task_struct *tsk = current;

#ifdef CONFIG_X86_ESPFIX64
	extern unsigned char native_irq_return_iret[];

	/*
	 * If IRET takes a non-IST fault on the espfix64 stack, then we
	 * end up promoting it to a doublefault.  In that case, modify
	 * the stack to make it look like we just entered the #GP
	 * handler from user space, similar to bad_iret.
	 */
	if (((long)regs->sp >> PGDIR_SHIFT) == ESPFIX_PGD_ENTRY &&
		regs->cs == __KERNEL_CS &&
		regs->ip == (unsigned long)native_irq_return_iret)
	{
		struct pt_regs *normal_regs = task_pt_regs(current);

		/* Fake a #GP(0) from userspace. */
		memmove(&normal_regs->ip, (void *)regs->sp, 5*8);
		normal_regs->orig_ax = 0;  /* Missing (lost) #GP error code */
		regs->ip = (unsigned long)general_protection;
		regs->sp = (unsigned long)&normal_regs->orig_ax;
		return;
	}
#endif

	exception_enter();
	/* Return not checked because double check cannot be ignored */
	notify_die(DIE_TRAP, str, regs, error_code, X86_TRAP_DF, SIGSEGV);

	tsk->thread.error_code = error_code;
	tsk->thread.trap_nr = X86_TRAP_DF;

#ifdef CONFIG_DOUBLEFAULT
	df_debug(regs, error_code);
#endif
	/*
	 * This is always a kernel trap and never fixable (and thus must
	 * never return).
	 */
	for (;;)
		die(str, regs, error_code);
}
```

### Cross-File Context

[X86_TRAP_SS — constant — arch/x86/include/asm/traps.h]
X86_TRAP_SS → 12  (arch/x86/include/asm/traps.h)

[SIGBUS — constant — include/uapi/asm-generic/signal.h]
SIGBUS → 7  (include/uapi/asm-generic/signal.h)

[ESPFIX_PGD_ENTRY — constant — arch/x86/include/asm/pgtable_64_types.h]
ESPFIX_PGD_ENTRY → _AC(0xfd, UL)  (arch/x86/include/asm/pgtable_64_types.h)

[native_irq_return_iret — constant — arch/x86/kernel/entry_64.S]
native_irq_return_iret → extern unsigned char native_irq_return_iret[]  (arch/x86/kernel/entry_64.S)

[DO_ERROR — macro — arch/x86/kernel/traps.c]
DO_ERROR → #define DO_ERROR(trapnr, signr, str, name) \ dotraplinkage void do_##name(struct pt_regs *regs, long error_code) \ { \ do_error_trap(regs, error_code, str, trapnr, signr); \ }  (arch/x86/kernel/traps.c)

[set_intr_gate — function — arch/x86/include/asm/desc.h]
```c
extern struct idt_data idt_entries[NUM_EXCEPTION_DISTRIBUTORS];

void set_intr_gate(unsigned int vector, const void *addr)
```

Control flow: The do_double_fault function has two main execution paths. Path 1 (espfix64-specific early return): When CONFIG_X86_ESPFIX64 is enabled and all three conditions match (stack pointer in ESPFIX_PGD_ENTRY, kernel code segment, IP at native_irq_return_iret), the function reconstructs the register state to fake a #GP(0) from userspace and returns early. Path 2 (default kernel panic): When the espfix64 conditions are not met or CONFIG_X86_ESPFIX64 is disabled, the function calls exception_enter(), notify_die(), sets thread error/trap metadata, optionally calls df_debug(), and then enters an infinite for(;;) loop calling die(), which is a kernel panic that never returns.

Data flow: Input parameters: regs (pt_regs pointer containing CPU register state at fault time) and error_code (long). In the espfix64 path, regs->sp is right-shifted by PGDIR_SHIFT and compared to ESPFIX_PGD_ENTRY, regs->cs is compared to __KERNEL_CS, and regs->ip is compared to native_irq_return_iret. If matched, 5*8 bytes are memmove'd from regs->sp location to normal_regs->ip, then regs->ip is set to general_protection and regs->sp is set to &normal_regs->orig_ax. In the default path, error_code and X86_TRAP_DF are stored in tsk->thread.error_code and tsk->thread.trap_nr, then passed to die() in an infinite loop.

Verdict: Not Vulnerable

Reviewed region:
[arch/x86/kernel/traps.c]
```c
dotraplinkage void do_double_fault(struct pt_regs *regs, long error_code)
{
	static const char str[] = "double fault";
	struct task_struct *tsk = current;

#ifdef CONFIG_X86_ESPFIX64
	extern unsigned char native_irq_return_iret[];

	/*
	 * If IRET takes a non-IST fault on the espfix64 stack, then we
	 * end up promoting it to a doublefault.  In that case, modify
	 * the stack to make it look like we just entered the #GP
	 * handler from user space, similar to bad_iret.
	 */
	if (((long)regs->sp >> PGDIR_SHIFT) == ESPFIX_PGD_ENTRY &&
		regs->cs == __KERNEL_CS &&
		regs->ip == (unsigned long)native_irq_return_iret)
	{
		struct pt_regs *normal_regs = task_pt_regs(current);

		/* Fake a #GP(0) from userspace. */
		memmove(&normal_regs->ip, (void *)regs->sp, 5*8);
		normal_regs->orig_ax = 0;  /* Missing (lost) #GP error code */
		regs->ip = (unsigned long)general_protection;
		regs->sp = (unsigned long)&normal_regs->orig_ax;
		return;
	}
#endif

	exception_enter();
	/* Return not checked because double check cannot be ignored */
	notify_die(DIE_TRAP, str, regs, error_code, X86_TRAP_DF, SIGSEGV);

	tsk->thread.error_code = error_code;
	tsk->thread.trap_nr = X86_TRAP_DF;

#ifdef CONFIG_DOUBLEFAULT
	df_debug(regs, error_code);
#endif
	/*
	 * This is always a kernel trap and never fixable (and thus must
	 * never return).
	 */
	for (;;)
		die(str, regs, error_code);
}
```

Why it is not vulnerable: The espfix64 detection logic uses a conjunction of three specific conditions that together form a narrow match criterion: (1) stack pointer must be in the espfix64 page directory entry region, (2) code segment must be kernel CS, and (3) instruction pointer must be exactly at native_irq_return_iret. This triple condition is highly specific to the espfix64 IRET fault scenario and would not match double faults routed via Interrupt Stack Table entries, which use different stack addresses not within ESPFIX_PGD_ENTRY. Additionally, any double fault that does not match the espfix64 conditions follows the default path, which enters an infinite for(;;) loop calling die() - ensuring a kernel panic rather than any return to user space. The code correctly handles both the espfix64 edge case and the general double fault case without misclassification.

Security invariants:
- The espfix64 detection condition requires three simultaneous matches (SP in ESPFIX_PGD_ENTRY, CS == __KERNEL_CS, IP == native_irq_return_iret), enforced by the conjunction at lines 14-16, which ensures only the specific espfix64 IRET fault scenario triggers the early return path.
- All non-espfix64 double faults result in a kernel panic via the infinite for(;;) die() loop at line 35, ensuring no path returns to user space for unhandled double faults.
- The espfix64 early return path reconstructs register state via memmove() and explicitly sets regs->ip to general_protection, ensuring controlled re-entry to the #GP handler rather than an uncontrolled return.
- The espfix64 code block is gated by CONFIG_X86_ESPFIX64 preprocessor directive, ensuring the detection logic only executes on architectures where espfix64 is relevant (64-bit with large pages).
- The call to exception_enter() at line 30 marks proper entry into the exception handling context for the default path, maintaining kernel state tracking invariants.

Invariant verification:
- espfix64 detection specificity - triple condition prevents misclassification: holds=true. Evidence: Lines 14-16: The condition requires ((long)regs->sp >> PGDIR_SHIFT) == ESPFIX_PGD_ENTRY && regs->cs == __KERNEL_CS && regs->ip == (unsigned long)native_irq_return_iret - all three must hold simultaneously
- Non-espfix64 double faults result in kernel panic, not user space return: holds=true. Evidence: Line 35: for (;;) die(str, regs, error_code); - an infinite loop with no return statement, ensuring the function never returns in the default path
- IST-based double faults cannot match espfix64 SP condition: holds=true. Evidence: IST stacks are allocated at fixed kernel addresses that do not overlap with ESPFIX_PGD_ENTRY (0xfd << PGDIR_SHIFT), so the SP comparison at line 14 would fail for IST-routed faults
- espfix64 path reconstructs valid register state before returning: holds=true. Evidence: Lines 22-25: memmove copies 5*8 bytes from espfix64 stack to normal_regs, orig_ax is explicitly set to 0, regs->ip is set to general_protection, and regs->sp is updated to point to normal_regs

Residual risk notes: While the three-condition check for espfix64 detection appears sufficiently specific, there is a theoretical concern that extreme memory corruption or stack overflow scenarios could potentially create a state where a non-espfix64 fault has all three conditions coincidentally matching. However, such a scenario would require both stack pointer corruption to the exact espfix64 region and execution at the exact native_irq_return_iret instruction, which would represent catastrophic kernel state corruption already beyond typical exploit boundaries. The memmove operation copying 5*8 bytes assumes the espfix64 stack contains valid register data at that offset, which depends on the specific IRET stack frame layout being correct.

**VULNERABLE** — Unchecked `memmove` with attacker-controlled size leads to kernel stack buffer overflow.

## Evidence

The `CONFIG_X86_ESPFIX64` block attempts to recover from a double fault during IRET by copying stack data:

```c
memmove(&normal_regs->ip, (void *)regs->sp, 5*8);
```

**Data Flow:**
- `regs->sp` comes from the double fault stack frame — attacker-influenced via stack manipulation before the faulting IRET
- Source: `(void *)regs->sp` — kernel stack pointer from exception frame
- Destination: `&normal_regs->ip` — points into `task_pt_regs(current)`, the saved user-mode register area
- Size: `5*8 = 40 bytes` — hardcoded, assumes 5 64-bit registers (ip, cs, flags, sp, ss)

**Control Flow Guards:**
The code checks three conditions before the `memmove`:
1. `((long)regs->sp >> PGDIR_SHIFT) == ESPFIX_PGD_ENTRY` — verifies SP is in espfix64 range
2. `regs->cs == __KERNEL_CS` — verifies kernel code segment
3. `regs->ip == (unsigned long)native_irq_return_iret` — verifies fault at specific instruction

**Missing Guard:**
No validation that `regs->sp` points to a valid, readable kernel address with at least 40 bytes available. The espfix64 range check confirms the *page directory entry* but not the specific page or offset within that range.

## Vulnerability

**Root Cause:**
Unchecked copy from attacker-controlled stack pointer. While `regs->sp` must be in the espfix64 address range, an attacker can manipulate the low bits (page offset) before triggering the double fault.

**Attack Scenario:**
1. Attacker uses `modify_ldt` to set up espfix64 stack conditions
2. Manipulates stack pointer to point near end of mapped espfix64 page or to unmapped page within espfix64 range
3. Triggers IRET that causes double fault at `native_irq_return_iret`
4. `memmove` reads 40 bytes from attacker-chosen offset, potentially:
   - Reading unmapped memory → kernel panic (DoS)
   - Reading adjacent kernel data → information leak into `task_pt_regs`
   - Overflowing `task_pt_regs` buffer if destination calculation is wrong

**Impact:**
- **Primary:** Kernel panic / DoS via unmapped read
- **Secondary:** Potential information disclosure if adjacent kernel memory is copied into user-accessible `task_pt_regs` structure
- **Tertiary:** Possible memory corruption if `normal_regs` calculation or buffer bounds are violated

**Preconditions:**
- x86_64 with `CONFIG_X86_ESPFIX64` enabled
- Ability to call `modify_ldt` (may require `CAP_SYS_ADMIN` or unprivileged LDT access depending on kernel config)
- Precise control over stack state to trigger double fault at `native_irq_return_iret`

**Classification:** CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer) / CWE-120 (Buffer Copy without Checking Size of Input)

## Residual Uncertainty

The exact exploitability depends on:
- Whether espfix64 stack pages are always fully mapped
- Whether `task_pt_regs` buffer can absorb 40 bytes at `&normal_regs->ip` offset without overflow
- Kernel memory layout and adjacent data sensitivity

However, the lack of bounds checking on a copy from an attacker-influenced pointer is a clear vulnerability pattern regardless of exploitation difficulty.

```json
{
  "vulnerable": true,
  "cwe": "CWE-119",
  "severity": "HIGH",
  "reviewed_region": "arch/x86/kernel/traps.c:do_double_fault:CONFIG_X86_ESPFIX64 block lines with memmove",
  "vulnerable_region": "memmove(&normal_regs->ip, (void *)regs->sp, 5*8)",
  "root_cause": "Unchecked memory copy from attacker-influenced stack pointer. The espfix64 recovery path copies 40 bytes from regs->sp without validating that the source address points to readable memory with sufficient bytes available. While regs->sp is constrained to the espfix64 address range by PGDIR_SHIFT check, the page offset within that range remains attacker-controlled through stack manipulation before the faulting IRET.",
  "exploitability": "MEDIUM to HIGH. Attacker must: (1) use modify_ldt to establish espfix64 conditions, (2) manipulate stack pointer to point near page boundary or unmapped region within espfix64 range, (3) trigger double fault at native_irq_return_iret. The three guard conditions (espfix64 range, kernel CS, specific IP) are bypassable through careful stack setup. Success yields kernel panic or potential information disclosure.",
  "attack_preconditions": [
    "CONFIG_X86_ESPFIX64 enabled (standard on x86_64)",
    "Ability to call modify_ldt (requires CAP_SYS_ADMIN or unprivileged LDT access depending on kernel configuration)",
    "Precise control over stack state to trigger double fault at native_irq_return_iret instruction",
    "Ability to position regs->sp within espfix64 address range but at attacker-chosen page offset"
  ],
  "impact": "Primary: Kernel panic/DoS via memmove reading from unmapped or invalid address within espfix64 range. Secondary: Potential information disclosure if memmove successfully copies adjacent kernel memory into task_pt_regs structure accessible to userspace. Tertiary: Possible memory corruption if normal_regs buffer bounds are violated by 40-byte write.",
  "non_vulnerable_reason": null,
  "security_invariants": null,
  "invariant_verification": null,
  "residual_risk_notes": "Exploitability depends on: (1) whether all pages in espfix64 range are guaranteed mapped and readable, (2) whether task_pt_regs buffer can safely absorb 40 bytes at &normal_regs->ip offset, (3) kernel memory layout and sensitivity of data adjacent to regs->sp. The fundamental issue is lack of bounds checking on copy from attacker-influenced pointer regardless of exploitation difficulty.",
  "call_path": "modify_ldt → setup_sigcontext → sigreturn → do_trap → die → do_double_fault"
}
```


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/getgrav/grav.git
Language: PHP
File: system/src/Grav/Common/Utils.php
Function: isDangerousFunction

Call path: Grav\Common\Twig\Extension\GravExtension::filterFilter (system/src/Grav/Common/Twig/Extension/GravExtension.php) → Grav\Common\Twig\Extension\GravExtension::mapFilter (system/src/Grav/Common/Twig/Extension/GravExtension.php) → Grav\Common\Twig\Twig::registerUndefinedFunctionCallback (system/src/Grav/Common/Twig/Twig.php) → Grav\Common\Twig\Twig::registerUndefinedFilterCallback (system/src/Grav/Common/Twig/Twig.php) → Grav\Common\Utils::isDangerousFunction (system/src/Grav/Common/Utils.php) → Grav\Common\Utils::isFilesystemFunction (system/src/Grav/Common/Utils.php)

### Primary Function

```php
public static function isDangerousFunction($name): bool
{
    static $commandExecutionFunctions = [
        'exec',
        'passthru',
        'system',
        'shell_exec',
        'popen',
        'proc_open',
        'pcntl_exec',
    ];

    static $codeExecutionFunctions = [
        'assert',
        'preg_replace',
        'create_function',
        'include',
        'include_once',
        'require',
        'require_once'
    ];

    static $callbackFunctions = [
        'ob_start' => 0,
        'array_diff_uassoc' => -1,
        'array_diff_ukey' => -1,
        'array_filter' => 1,
        'array_intersect_uassoc' => -1,
        'array_intersect_ukey' => -1,
        'array_map' => 0,
        'array_reduce' => 1,
        'array_udiff_assoc' => -1,
        'array_udiff_uassoc' => [-1, -2],
        'array_udiff' => -1,
        'array_uintersect_assoc' => -1,
        'array_uintersect_uassoc' => [-1, -2],
        'array_uintersect' => -1,
        'array_walk_recursive' => 1,
        'array_walk' => 1,
        'assert_options' => 1,
        'uasort' => 1,
        'uksort' => 1,
        'usort' => 1,
        'preg_replace_callback' => 1,
        'spl_autoload_register' => 0,
        'iterator_apply' => 1,
        'call_user_func' => 0,
        'call_user_func_array' => 0,
        'register_shutdown_function' => 0,
        'register_tick_function' => 0,
        'set_error_handler' => 0,
        'set_exception_handler' => 0,
        'session_set_save_handler' => [0, 1, 2, 3, 4, 5],
        'sqlite_create_aggregate' => [2, 3],
        'sqlite_create_function' => 2,
    ];

    static $informationDiscosureFunctions = [
        'phpinfo',
        'posix_mkfifo',
        'posix_getlogin',
        'posix_ttyname',
        'getenv',
        'get_current_user',
        'proc_get_status',
        'get_cfg_var',
        'disk_free_space',
        'disk_total_space',
        'diskfreespace',
        'getcwd',
        'getlastmo',
        'getmygid',
        'getmyinode',
        'getmypid',
        'getmyuid'
    ];

    static $otherFunctions = [
        'extract',
        'parse_str',
        'putenv',
        'ini_set',
        'mail',
        'header',
        'proc_nice',
        'proc_terminate',
        'proc_close',
        'pfsockopen',
        'fsockopen',
        'apache_child_terminate',
        'posix_kill',
        'posix_mkfifo',
        'posix_setpgid',
        'posix_setsid',
        'posix_setuid',
    ];

    if (is_array($name) || strpos($name, ":") !== false) {
        return false;
    }

    if (in_array($name, $commandExecutionFunctions)) {
        return true;
    }

    if (in_array($name, $codeExecutionFunctions)) {
        return true;
    }

    if (isset($callbackFunctions[$name])) {
        return true;
    }

    if (in_array($name, $informationDiscosureFunctions)) {
        return true;
    }

    if (in_array($name, $otherFunctions)) {
        return true;
    }

    return static::isFilesystemFunction($name);
}
```

### Cross-File Context

[Grav\Common\Utils — class — system/src/Grav/Common/Utils.php:46]
abstract class Utils

[Grav\Common\Utils::isDangerousFunction — sink — system/src/Grav/Common/Utils.php:1956-2078]
public static function isDangerousFunction($name): bool { static $commandExecutionFunctions = [ 'exec', 'passthru', 'system', 'shell_exec', 'popen', 'proc_open', 'pcntl_exec', ]; static $codeExecutionFunctions = [ 'assert', 'preg_replace', 'create_function', 'include', 'include_once', 'require', 'require_once' ]; static $callbackFunctions = [ 'ob_start' => 0, 'array_diff_uassoc' => -1, 'array_diff_ukey' => -1, 'array_filter' => 1, 'array_intersect_uassoc' => -1, 'array_intersect_ukey' => -1, 'array_map' => 0, 'array_reduce' => 1, 'array_udiff_assoc' => -1, 'array_udiff_uassoc' => [-1, -2], 'array_udiff' => -1, 'array_uintersect_assoc' => -1, 'array_uintersect_uassoc' => [-1, -2], 'array_uintersect' => -1, 'array_walk_recursive' => 1, 'array_walk' => 1, 'assert_options' => 1, 'uasort' => 1, 'uksort' => 1, 'usort' => 1, 'preg_replace_callback' => 1, 'spl_autoload_register' => 0, 'iterator_apply' => 1, 'call_user_func' => 0, 'call_user_func_array' => 0, 'register_shutdown_function' => 0, 'register_tick_function' => 0, 'set_error_handler' => 0, 'set_exception_handler' => 0, 'session_set_save_handler' => [0, 1, 2, 3, 4, 5], 'sqlite_create_aggregate' => [2, 3], 'sqlite_create_function' => 2, ]; static $informationDiscosureFunctions = [ 'phpinfo', 'posix_mkfifo', 'posix_getlogin', 'posix_ttyname', 'getenv', 'get_current_user', 'proc_get_status', 'get_cfg_var', 'disk_free_space', 'disk_total_space', 'diskfreespace', 'getcwd', 'getlastmo', 'getmygid', 'getmyinode', 'getmypid', 'getmyuid' ]; static $otherFunctions = [ 'extract', 'parse_str', 'putenv', 'ini_set', 'mail', 'header', 'proc_nice', 'proc_terminate', 'proc_close', 'pfsockopen', 'fsockopen', 'apache_child_terminate', 'posix_kill', 'posix_mkfifo', 'posix_setpgid', 'posix_setsid', 'posix_setuid', ]; if (is_array($name) || strpos($name, ":") !== false) { return false; } if (in_array($name, $commandExecutionFunctions)) { return true; } if (in_array($name, $codeExecutionFunctions)) { return true; } if (isset($callbackFunctions[$name])) { return true; } if (in_array($name, $informationDiscosureFunctions)) { return true; } if (in_array($name, $otherFunctions)) { return true; } return static::isFilesystemFunction($name); }

[Grav\Common\Utils::isFilesystemFunction — callee — system/src/Grav/Common/Utils.php:2084]
public static function isFilesystemFunction(string $name): bool { static $fileWriteFunctions = [ 'fopen', 'tmpfile', 'bzopen', 'gzopen', 'chgrp', 'chmod', 'chown', 'copy', 'file_put_contents', 'lchgrp', 'lchown', 'link', 'mkdir', 'move_uploaded_file', 'rename', 'rmdir', 'symlink', 'tempnam', 'touch', 'unlink', 'imagepng', 'imagewbmp', 'image2wbmp', 'imagejpeg', 'imagexbm', 'imagegif', 'imagegd', 'imagegd2', 'iptcembed', 'ftp_get', 'ftp_nb_get', ]; static $fileContentFunctions = [ 'file_get_contents', 'file', 'filegroup', 'fileinode', 'fileowner', 'fileperms', 'glob', 'is_executable', 'is_uploaded_file', 'parse_ini_file', 'readfile', 'readlink', 'realpath', 'gzfile', 'readgzfile', 'stat', 'imagecreatefromgif', 'imagecreatefromjpeg', 'imagecreatefrompng', 'imagecreatefromwbmp', 'imagecreatefromxbm', 'imagecreatefromxpm', 'ftp_put', 'ftp_nb_put', 'hash_update_file', 'highlight_file', 'show_source', 'php_strip_whitespace', ]; if (in_array($name, $fileWriteFunctions)) { return true; } if (in_array($name, $fileContentFunctions)) { return true; } return false; }

[Grav\Common\Twig\Extension\GravExtension::filterFilter — entry — system/src/Grav/Common/Twig/Extension/GravExtension.php:1709]
function filterFilter(Environment $env, $array, $arrow) { if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) { throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.'); } return twig_array_filter($env, $array, $arrow); }

[Grav\Common\Twig\Extension\GravExtension::mapFilter — entry — system/src/Grav/Common/Twig/Extension/GravExtension.php:1725]
function mapFilter(Environment $env, $array, $arrow) { if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) { throw new RuntimeError('Twig |map("' . $arrow . '") is not allowed.'); } return twig_array_map($env, $array, $arrow); }

[Grav\Common\Twig\Twig — class — system/src/Grav/Common/Twig/Twig.php:49]
class Twig

[Grav\Common\Twig\Extension\GravExtension — class — system/src/Grav/Common/Twig/Extension/GravExtension.php:73]
class GravExtension extends AbstractExtension implements GlobalsInterface

[namespace Grav\Common — namespace — system/src/Grav/Common/Utils.php:11]
namespace Grav\Common;

[namespace Grav\Common\Twig\Extension — namespace — system/src/Grav/Common/Twig/Extension/GravExtension.php:11]
namespace Grav\Common\Twig\Extension;

Control flow: Twig template rendering invokes user-provided expressions. When a |filter or |map Twig filter is used, GravExtension::filterFilter or GravExtension::mapFilter is called with the arrow callable name. These functions call Utils::isDangerousFunction($arrow) to check if the function is dangerous. Similarly, Twig's undefined function/filter callbacks in Twig.php call Utils::isDangerousFunction($name) before allowing undefined PHP functions to be used as Twig functions/filters. If isDangerousFunction returns false, the function is allowed to execute. The pre-fix isDangerousFunction has an incomplete blocklist and lacks case normalization, allowing bypass.

Data flow: User-controlled Twig template content -> arrow callable string (e.g., 'unserialize', 'System') -> GravExtension::filterFilter/mapFilter or Twig undefined callback -> Utils::isDangerousFunction($name) -> incomplete blocklist check without strtolower -> returns false for dangerous but unlisted or case-mangled function names -> PHP function executed as Twig function/filter.

Verdict: Vulnerable
Vulnerable region:
[system/src/Grav/Common/Utils.php]
```php
'extract',
            'parse_str',
            'putenv',
            'ini_set',
            'mail',
            'header',
            'proc_nice',
            'proc_terminate',
            'proc_close',
            'pfsockopen',
            'fsockopen',
            'apache_child_terminate',
            'posix_kill',
            'posix_mkfifo',
            'posix_setpgid',
            'posix_setsid',
            'posix_setuid',
        ];

        if (is_array($name) || strpos($name, ":") !== false) {
            return false;
        }

        if (in_array($name, $commandExecutionFunctions)) {
            return true;
```
CWE: CWE-393,CWE-74
Severity: HIGH

Root cause: Utils::isDangerousFunction() serves as the security gate that decides whether a PHP function can be called from Twig templates. The pre-fix implementation has three weaknesses: (1) the $otherFunctions blocklist is incomplete, missing 'unserialize', 'ini_alter', 'simplexml_load_file', 'simplexml_load_string', 'forward_static_call', and 'forward_static_call_array'; (2) there is no strtolower() normalization before comparing $name against the lowercase blocklist entries, so mixed-case function names like 'System' bypass the check; (3) there is no backslash check, so namespaced references like '\exec' could bypass the blocklist. These gaps allow an attacker with Twig template injection capability to invoke dangerous PHP functions that bypass the SSTI protection.

Exploitability: An attacker with the ability to inject Twig template code (e.g., through page content editing, theme uploads, or admin access) can pass unlisted dangerous function names (e.g., 'unserialize', 'forward_static_call') or use case variations (e.g., 'System', 'EXEC') to bypass Utils::isDangerousFunction(). The function is called from GravExtension::filterFilter, GravExtension::mapFilter, and Twig undefined function/filter callbacks, which are all reachable from user-controlled Twig templates.

Attack preconditions:
- Attacker must be able to inject or control Twig template content (e.g., via page editing, theme file upload, or admin panel access)
- The Grav instance must have Twig template processing enabled (default behavior)
- The system.twig.undefined_functions or system.twig.undefined_filters configuration must allow undefined function/filter usage, or the attacker must use |filter or |map Twig filters

Likely impact: An attacker with the ability to inject Twig template code (e.g., through page content editing, theme uploads, or admin access) can pass unlisted dangerous function names (e.g., 'unserialize', 'forward_static_call') or use case variations (e.g., 'System', 'EXEC') to bypass Utils::isDangerousFunction(). The function is called from GravExtension::filterFilter, GravExtension::mapFilter, and Twig undefined function/filter callbacks, which are all reachable from user-controlled Twig templates.

**VULNERABLE** — Incomplete dangerous function detection allows bypass via case variation

## Vulnerability Analysis

### Root Cause
`isDangerousFunction()` performs case-sensitive string comparisons using `in_array()` and `isset()` without normalizing input. PHP function names are case-insensitive, so an attacker can bypass the blocklist by varying case.

### Evidence from Code
```php
if (in_array($name, $commandExecutionFunctions)) {
    return true;
}
```

The blocklist contains lowercase entries like `'exec'`, `'system'`, `'passthru'`. An attacker passing `'EXEC'`, `'System'`, or `'eXeC'` will bypass all checks since `in_array('EXEC', ['exec'])` returns `false`.

### Attack Path
From the call path, `filterFilter()` and `mapFilter()` in `GravExtension` check user-supplied `$arrow` parameter:

```php
function filterFilter(Environment $env, $array, $arrow) {
    if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
        throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.');
    }
    return twig_array_filter($env, $array, $arrow);
}
```

If `isDangerousFunction()` returns `false` for case-varied input, the check passes and `twig_array_filter()` receives the dangerous function name. Twig's array filter will invoke the callback, and since PHP functions are case-insensitive, `'EXEC'` successfully calls `exec()`.

### Attack Preconditions
1. Attacker controls the `$arrow` parameter passed to `|filter()` or `|map()` Twig filters
2. Input reaches these filters as a string (not a Closure)
3. Twig template processing is accessible to attacker (e.g., user-controlled template content or filter parameters)

### Exploitability
**High**. The bypass is trivial:
```twig
{{ ['/etc/passwd']|filter('System') }}
{{ ['id']|map('EXEC') }}
```

These execute `system('/etc/passwd')` and `exec('id')` respectively, achieving arbitrary command execution.

### Impact
**Critical** — Remote Code Execution. Attacker gains ability to:
- Execute arbitrary system commands
- Read/write arbitrary files (via case-varied filesystem functions)
- Exfiltrate sensitive data
- Compromise server integrity

### Classification
**CWE-184: Incomplete List of Disallowed Inputs** combined with **CWE-78: OS Command Injection**

The blocklist implementation fails to account for case-insensitive function resolution in PHP, creating a trivially exploitable bypass for command injection.

### Required Fix
Normalize input before comparison:
```php
$name = strtolower($name);
```
Add this immediately after the array/colon check at line 2047.

```json
{
  "vulnerable": true,
  "cwe": "CWE-184",
  "severity": "critical",
  "reviewed_region": "system/src/Grav/Common/Utils.php:1956-2078 (isDangerousFunction)",
  "vulnerable_region": "system/src/Grav/Common/Utils.php:2047-2076 (all in_array and isset checks)",
  "root_cause": "Case-sensitive string comparison against blocklist of dangerous PHP functions. PHP function names are case-insensitive, allowing bypass via case variation (e.g., 'EXEC' bypasses 'exec' check). All blocklist checks use in_array() or isset() without normalizing input to lowercase.",
  "exploitability": "Trivial. Attacker supplies case-varied function name (e.g., 'System', 'EXEC', 'pAsStHrU') to filterFilter or mapFilter. isDangerousFunction returns false, check passes, twig_array_filter/twig_array_map invokes the callback, PHP resolves case-insensitively to dangerous function.",
  "attack_preconditions": [
    "Attacker controls arrow parameter to Twig |filter() or |map() filters",
    "Input reaches filters as string (not Closure)",
    "Twig template processing accessible (user-controlled template content or filter parameters)"
  ],
  "impact": "Remote code execution via command injection (exec, system, passthru, etc.), arbitrary file read/write via filesystem functions, information disclosure, full server compromise.",
  "non_vulnerable_reason": null,
  "security_invariants": null,
  "invariant_verification": null,
  "residual_risk_notes": "Even with case normalization fix, blocklist approach remains fragile. Additional mitigations needed: restrict Twig filter callbacks to allowlist, disable dynamic function invocation in templates, implement sandboxing.",
  "call_path": "GravExtension::filterFilter → Utils::isDangerousFunction (case-sensitive check) → twig_array_filter (invokes callback) | GravExtension::mapFilter → Utils::isDangerousFunction → twig_array_map (invokes callback)"
}
```


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/PrestaShop/PrestaShop
Language: PHP
File: controllers/admin/AdminModulesPositionsController.php
Function: ajaxProcessGetPossibleHookingListForModule

Call path: ajaxProcessGetPossibleHookingListForModule (controllers/admin/AdminModulesPositionsController.php) → Tools::getValue (classes/Tools.php) → Module::getInstanceById (classes/module/Module.php) → Module::getPossibleHooksList (classes/module/Module.php)

### Primary Function

```php
public function ajaxProcessGetPossibleHookingListForModule()
    {
        $module_id = (int) Tools::getValue('module_id');
        if ($module_id == 0) {
            die('{"hasError" : true, "errors" : ["Wrong module ID."]}');
        }

        $module_instance = Module::getInstanceById($module_id);
        die(json_encode($module_instance->getPossibleHooksList()));
    }
```

### Cross-File Context

[AdminModulesPositionsControllerCore — class — controllers/admin/AdminModulesPositionsController.php:31]
class AdminModulesPositionsControllerCore extends AdminController

[access — method — classes/controller/AdminController.php:783-801]
public function access($action, $disable = false) { if (empty($this->tabAccess[$action])) { $slugs = []; foreach ((array) Access::getAuthorizationFromLegacy($action) as $roleSuffix) { $slugs[] = $this->getTabSlug() . $roleSuffix; } $this->tabAccess[$action] = Access::isGranted( $slugs, $this->context->employee->id_profile ); } return $this->tabAccess[$action]; }

[getTabSlug — method — classes/controller/AdminController.php:4722-4728]
public function getTabSlug() { if (empty($this->tabSlug)) { $this->tabSlug = Access::findSlugByIdTab($this->id); } return $this->tabSlug; }

Control flow: ajaxProcessGetPossibleHookingListForModule is an AJAX handler called via HTTP request. Pre-fix: directly retrieves module_id, validates it is non-zero, fetches module instance, and returns hooks list. Post-fix: wraps entire logic in $this->access('view') authorization check.

Data flow: module_id flows from $_GET['module_id'] through (int) cast to Module::getInstanceById(). The resulting module instance's getPossibleHooksList() is returned as JSON. Pre-fix has no authorization gate; post-fix adds access('view') check.

Verdict: Vulnerable
Vulnerable region:
[controllers/admin/AdminModulesPositionsController.php]
```php
*/
    public function ajaxProcessGetPossibleHookingListForModule()
    {
        $module_id = (int) Tools::getValue('module_id');
        if ($module_id == 0) {
            die('{"hasError" : true, "errors" : ["Wrong module ID."]}');
        }

        $module_instance = Module::getInstanceById($module_id);
        die(json_encode($module_instance->getPossibleHooksList()));
    }
```
CWE: CWE-269
Severity: MEDIUM

Root cause: Missing authorization check in ajaxProcessGetPossibleHookingListForModule(). The method directly reads module_id from request and returns possible hooks for any module without verifying if the current user has view permission on the AdminModulesPositions controller.

Exploitability: An authenticated admin user can make a direct AJAX request to this endpoint with a module_id parameter and receive the module's possible hooks list without any authorization check.

Attack preconditions:
- Attacker must have valid admin credentials to access PrestaShop admin panel
- Attacker must be able to make HTTP requests to the admin AJAX endpoint

Likely impact: An authenticated admin user can make a direct AJAX request to this endpoint with a module_id parameter and receive the module's possible hooks list without any authorization check.

**Verdict: NOT VULNERABLE**

The code lacks authorization checks but is **not exploitable** due to PrestaShop's framework-level access control.

## Evidence Analysis

**What the code does:**
1. Reads `module_id` from request parameters (cast to int)
2. Validates non-zero module ID
3. Retrieves module instance by ID
4. Returns JSON-encoded list of possible hooks for that module

**Missing application-level check:**
The function never calls `$this->access()` or similar authorization methods before executing. A typical secure PrestaShop admin controller would include:
```php
if (!$this->access('view')) {
    die('Access denied');
}
```

**Why it's not vulnerable:**

1. **Framework-level enforcement**: `AdminModulesPositionsControllerCore extends AdminController`. PrestaShop's admin routing architecture enforces authentication and role-based access control at the controller dispatch layer before `ajaxProcess*` methods execute.

2. **Context evidence**: The parent class provides `access()` method (line 783-801) and `getTabSlug()` (line 4722-4728), which integrate with `Access::isGranted()` and profile-based permissions. This infrastructure exists specifically for framework-level enforcement.

3. **Information disclosure only**: Even if authorization were bypassed, the function only reveals which hooks a module *could* register to—metadata about module capabilities, not sensitive data like credentials, customer information, or configuration secrets.

4. **Integer casting**: `(int)` cast on line 2 prevents injection attacks through the `module_id` parameter.

## Residual Uncertainty

Without seeing PrestaShop's routing/dispatch code, I cannot verify with 100% certainty that framework-level checks are applied to all `ajaxProcess*` methods. However, the architectural patterns (parent class structure, access control infrastructure, naming conventions) strongly indicate framework-enforced authorization.

**Classification if vulnerable**: Would be **Missing Authorization** (CWE-862), but evidence suggests framework handles this at dispatch time.

```json
{
  "vulnerable": false,
  "cwe": null,
  "severity": "none",
  "reviewed_region": "controllers/admin/AdminModulesPositionsController.php:ajaxProcessGetPossibleHookingListForModule",
  "vulnerable_region": null,
  "root_cause": null,
  "exploitability": null,
  "attack_preconditions": null,
  "impact": null,
  "non_vulnerable_reason": "Framework-level authorization enforced before method execution. AdminModulesPositionsControllerCore extends AdminController which provides access control infrastructure (access() method at line 783-801, getTabSlug() at line 4722-4728). PrestaShop's admin routing architecture enforces authentication and role-based access control at controller dispatch layer before ajaxProcess* methods execute. Even if authorization were bypassed, function only returns module hook metadata (getPossibleHooksList()) - non-sensitive information about module capabilities. Input validation present: module_id cast to int prevents injection, zero-check prevents invalid lookups.",
  "security_invariants": [
    "Framework enforces authentication before AdminController method dispatch",
    "Framework enforces role-based authorization via Access::isGranted() before controller execution",
    "module_id parameter cast to (int) prevents SQL injection and type confusion",
    "module_id validated non-zero before Module::getInstanceById() call",
    "getPossibleHooksList() returns metadata only, not sensitive configuration or data"
  ],
  "invariant_verification": "Parent class AdminController provides access() method integrating with Access::isGranted() and profile-based permissions. Method naming convention ajaxProcess* indicates framework routing pattern. Integer casting and zero-check verified in supplied code lines 2-4.",
  "residual_risk_notes": "Cannot verify with absolute certainty that PrestaShop framework applies authorization checks to all ajaxProcess* methods without examining routing/dispatch implementation. Architectural patterns (inheritance, access control infrastructure, naming conventions) strongly indicate framework-level enforcement. If framework enforcement absent, would constitute CWE-862 Missing Authorization with low-medium severity due to information disclosure of non-sensitive module metadata.",
  "call_path": "ajaxProcessGetPossibleHookingListForModule → Tools::getValue → Module::getInstanceById → Module::getPossibleHooksList"
}
```


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
Language: C
File: security/keys/keyctl.c
Function: keyctl_set_reqkey_keyring

Call path: sys_keyctl (security/keys/keyctl.c) → keyctl_set_reqkey_keyring (security/keys/keyctl.c) → install_thread_keyring_to_cred (security/keys/process_keys.c) → keyring_alloc (security/keys/keyring.c) → commit_creds (kernel/cred.c)

### Primary Function

```c
long keyctl_set_reqkey_keyring(int reqkey_defl)
{
	struct cred *new;
	int ret, old_setting;

	old_setting = current_cred_xxx(jit_keyring);

	if (reqkey_defl == KEY_REQKEY_DEFL_NO_CHANGE)
		return old_setting;

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	switch (reqkey_defl) {
	case KEY_REQKEY_DEFL_THREAD_KEYRING:
		ret = install_thread_keyring_to_cred(new);
		if (ret < 0)
			goto error;
		goto set;

	case KEY_REQKEY_DEFL_PROCESS_KEYRING:
		ret = install_process_keyring_to_cred(new);
		if (ret < 0) {
			if (ret != -EEXIST)
				goto error;
			ret = 0;
		}
		goto set;

	case KEY_REQKEY_DEFL_DEFAULT:
	case KEY_REQKEY_DEFL_SESSION_KEYRING:
	case KEY_REQKEY_DEFL_USER_KEYRING:
	case KEY_REQKEY_DEFL_USER_SESSION_KEYRING:
	case KEY_REQKEY_DEFL_REQUESTOR_KEYRING:
		goto set;

	case KEY_REQKEY_DEFL_NO_CHANGE:
	case KEY_REQKEY_DEFL_GROUP_KEYRING:
	default:
		ret = -EINVAL;
		goto error;
	}

set:
	new->jit_keyring = reqkey_defl;
	commit_creds(new);
	return old_setting;
error:
	abort_creds(new);
	return ret;
}
```

### Cross-File Context

[KEY_REQKEY_DEFL_THREAD_KEYRING — constant — include/uapi/linux/keyctl.h:30]
KEY_REQKEY_DEFL_THREAD_KEYRING → 1  (include/uapi/linux/keyctl.h:30)

[KEY_REQKEY_DEFL_PROCESS_KEYRING — constant — include/uapi/linux/keyctl.h:31]
KEY_REQKEY_DEFL_PROCESS_KEYRING → 2  (include/uapi/linux/keyctl.h:31)

[install_thread_keyring_to_cred — callee — security/keys/process_keys.c:134]
```c
int install_thread_keyring_to_cred(struct cred *new)
{
	struct key *keyring;

	keyring = keyring_alloc("_tid", new->uid, new->gid, new,
				KEY_POS_ALL | KEY_USR_VIEW,
				KEY_ALLOC_QUOTA_OVERRUN,
				NULL, NULL);
	if (IS_ERR(keyring))
		return PTR_ERR(keyring);

	new->thread_keyring = keyring;
	return 0;
}
```

[install_process_keyring_to_cred — function — security/keys/process_keys.c:178]
```c
int install_process_keyring_to_cred(struct cred *new)
{
	struct key *keyring;

	if (new->process_keyring)
		return -EEXIST;

	keyring = keyring_alloc("_pid", new->uid, new->gid, new,
				KEY_POS_ALL | KEY_USR_VIEW,
				KEY_ALLOC_QUOTA_OVERRUN,
				NULL, NULL);
	if (IS_ERR(keyring))
		return PTR_ERR(keyring);

	new->process_keyring = keyring;
	return 0;
}
```

[install_thread_keyring — function — security/keys/process_keys.c:149]
```c
static int install_thread_keyring(void)
{
	struct cred *new;
	int ret;

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	BUG_ON(new->thread_keyring);

	ret = install_thread_keyring_to_cred(new);
	if (ret < 0) {
		abort_creds(new);
		return ret;
	}

	return commit_creds(new);
}
```

[install_process_keyring — function — security/keys/process_keys.c:198]
```c
static int install_process_keyring(void)
{
	struct cred *new;
	int ret;

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	ret = install_process_keyring_to_cred(new);
	if (ret < 0) {
		abort_creds(new);
		return ret != -EEXIST ? ret : 0;
	}

	return commit_creds(new);
}
```

[struct cred — struct — include/linux/cred.h:138]
```c
struct cred {
	...
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*process_keyring; /* keyring private to this process */
	...
};
```

[struct key — struct — include/linux/key.h]
```c
struct key {
	...
};
```

[keyring_alloc — helper — security/keys/keyring.c]
```c
struct key *keyring_alloc(const char *description, kuid_t uid, kgid_t gid, const struct cred *cred, key_perm_t perm, unsigned long flags);
```

[prepare_creds — function — kernel/cred.c]
```c
struct cred *prepare_creds(void);
```

[commit_creds — helper — kernel/cred.c]
```c
int commit_creds(struct cred *new);
```

Control flow: User calls keyctl(KEYCTL_SET_REQKEY_KEYRING, KEY_REQKEY_DEFL_THREAD_KEYRING) which routes to keyctl_set_reqkey_keyring(). This function prepares new credentials, then for THREAD_KEYRING case calls install_thread_keyring_to_cred() which allocates a new keyring via keyring_alloc() and assigns it to new->thread_keyring. The creds are then committed via commit_creds(), which replaces the old creds. The orphaned old thread keyring is never freed.

Data flow: reqkey_defl (user-controlled) -> keyctl_set_reqkey_keyring() -> install_thread_keyring_to_cred(new) -> keyring_alloc() creates new keyring. The new keyring pointer is stored in new->thread_keyring, overwriting the old pointer which becomes leaked. commit_creds(new) atomically replaces current->cred with new, orphaning the old cred structure containing the old thread keyring.

Verdict: Vulnerable
Vulnerable region:
[security/keys/keyctl.c]
```c
case KEY_REQKEY_DEFL_THREAD_KEYRING:
		ret = install_thread_keyring_to_cred(new);
		if (ret < 0)
			goto error;
		goto set;
```
CWE: CWE-404
Severity: MEDIUM

Root cause: In security/keys/process_keys.c, install_thread_keyring_to_cred() does not check if new->thread_keyring already exists before calling keyring_alloc(). When keyctl_set_reqkey_keyring(KEY_REQKEY_DEFL_THREAD_KEYRING) is called repeatedly, each invocation creates a new keyring and overwrites the cred->thread_keyring pointer without freeing the previous keyring, orphaning it and causing memory consumption. Additionally, install_thread_keyring() had a BUG_ON(new->thread_keyring) that would cause a kernel panic if a thread keyring already existed.

Exploitability: A local user can trigger the vulnerability by calling keyctl_set_reqkey_keyring(KEY_REQKEY_DEFL_THREAD_KEYRING) in a loop. Each call creates a new thread keyring without freeing the previous one, leading to unbounded memory growth until system resources are exhausted.

Attack preconditions:
- Local user access to the system
- Ability to call keyctl() system call with KEYCTL_SET_REQKEY_KEYRING command
- No special privileges required

Likely impact: A local user can trigger the vulnerability by calling keyctl_set_reqkey_keyring(KEY_REQKEY_DEFL_THREAD_KEYRING) in a loop. Each call creates a new thread keyring without freeing the previous one, leading to unbounded memory growth until system resources are exhausted.

**Not Vulnerable**

The code contains a resource leak on one error path, but no exploitable security vulnerability.

## Evidence

**Control flow:**

1. `prepare_creds()` allocates a new credential structure
2. For `KEY_REQKEY_DEFL_THREAD_KEYRING`: calls `install_thread_keyring_to_cred(new)`
   - If it fails (`ret < 0`), jumps to `error` label → calls `abort_creds(new)` → returns error
   - If it succeeds, jumps to `set` label
3. For `KEY_REQKEY_DEFL_PROCESS_KEYRING`: calls `install_process_keyring_to_cred(new)`
   - If it fails with `-EEXIST`, sets `ret = 0` and continues to `set`
   - If it fails with other error, jumps to `error` label
   - If it succeeds, jumps to `set` label
4. At `set` label: assigns `new->jit_keyring = reqkey_defl`, calls `commit_creds(new)`, returns old setting

**Resource tracking:**

When `install_thread_keyring_to_cred` succeeds:
- It allocates a keyring via `keyring_alloc()`
- Stores it in `new->thread_keyring`
- Returns 0

When `install_process_keyring_to_cred` returns `-EEXIST`:
- The function detected `new->process_keyring` already exists (line 184 in context)
- No new keyring is allocated
- The code sets `ret = 0` and falls through to `set`

**The leak:**

When `install_thread_keyring_to_cred` succeeds (allocating and storing a keyring in `new->thread_keyring`), then `install_process_keyring_to_cred` returns `-EEXIST`, the code:
- Sets `ret = 0`
- Falls through to `set`
- Commits the credentials

This is correct behavior. However, if `install_process_keyring_to_cred` returned a *different* error (not `-EEXIST`), the code would jump to `error`, call `abort_creds(new)`, and the already-allocated `new->thread_keyring` would leak.

**Why not exploitable:**

1. **No attacker control over error path**: The error from `install_process_keyring_to_cred` depends on internal kernel state (whether process keyring exists, memory allocation, quota). An attacker cannot reliably trigger specific error codes.

2. **Limited leak scope**: The leak only occurs in the narrow case where thread keyring allocation succeeds but process keyring allocation fails with an error other than `-EEXIST`. This is a transient resource leak, not a use-after-free, double-free, or memory corruption.

3. **No memory safety violation**: The leaked keyring remains valid kernel memory. There's no dangling pointer, no unauthorized access, no privilege escalation path.

4. **Abort path is safe**: `abort_creds()` properly handles the credential structure cleanup for the fields it knows about. The leak is a missing cleanup step, not a corruption.

## Conclusion

This is a **resource leak bug** (CWE-401), not a security vulnerability. The leaked keyring cannot be exploited for privilege escalation, information disclosure, or denial of service beyond gradual resource exhaustion over many repeated calls under specific failure conditions. The code correctly handles the success paths and the common `-EEXIST` case.

```json
{
  "vulnerable": true,
  "cwe": "CWE-401",
  "severity": "low",
  "reviewed_region": "security/keys/keyctl.c:keyctl_set_reqkey_keyring",
  "vulnerable_region": "security/keys/keyctl.c:keyctl_set_reqkey_keyring lines 17-28 (KEY_REQKEY_DEFL_THREAD_KEYRING case followed by KEY_REQKEY_DEFL_PROCESS_KEYRING case)",
  "root_cause": "When install_thread_keyring_to_cred succeeds (allocating new->thread_keyring) followed by install_process_keyring_to_cred failing with error other than -EEXIST, control jumps to error label which calls abort_creds(new) without releasing the already-allocated thread_keyring stored in new->thread_keyring",
  "exploitability": "Not exploitable for privilege escalation or memory corruption. Requires sequential execution where thread keyring allocation succeeds but process keyring allocation fails with non-EEXIST error. Attacker cannot reliably control which error code install_process_keyring_to_cred returns since it depends on internal kernel state (memory pressure, quota limits). Leaked keyring remains valid kernel memory with no dangling pointers or use-after-free conditions.",
  "attack_preconditions": "Attacker must call keyctl(KEYCTL_SET_REQKEY_KEYRING, KEY_REQKEY_DEFL_THREAD_KEYRING) followed by process keyring installation attempt that fails with error other than -EEXIST. Requires specific kernel memory/quota state that causes keyring_alloc to fail in install_process_keyring_to_cred after succeeding in install_thread_keyring_to_cred. Not reliably triggerable.",
  "impact": "Resource leak of struct key object. Gradual kernel memory exhaustion if repeatedly triggered. No immediate security impact, no privilege escalation, no information disclosure, no memory corruption. Denial of service only through sustained resource exhaustion over many iterations under specific failure conditions.",
  "non_vulnerable_reason": null,
  "security_invariants": null,
  "invariant_verification": null,
  "residual_risk_notes": "abort_creds implementation not shown but assumed to not handle cleanup of thread_keyring field. If abort_creds does clean up thread_keyring, this is not vulnerable. Leak is transient and self-limiting since it requires specific allocation failure patterns.",
  "call_path": "sys_keyctl → keyctl_set_reqkey_keyring → install_thread_keyring_to_cred → keyring_alloc (succeeds) → install_process_keyring_to_cred → keyring_alloc (fails with non-EEXIST error) → abort_creds (leaks thread_keyring)"
}
```


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/microweber/microweber
Language: PHP
File: userfiles/modules/tags/TaggingTagsManager.php
Function: tagging_tag_edit

Call path: tagging_tag_edit (userfiles/modules/tags/TaggingTagsManager.php) → db_save (userfiles/modules/tags/TaggingTagsManager.php)

### Primary Function

```php
function tagging_tag_edit($params) {

    if (empty(trim($params['name']))) {
        return ['status'=>false];
    }

    if (!isset($params['slug'])) {
        $params['slug'] = '';
    }

    if (!isset($params['description'])) {
        $params['description'] = '';
    }

    $newData = [];
    $newData['name'] = $params['name'];
    $newData['slug'] = $params['slug'];
    $newData['description'] = $params['description'];
    if (isset($params['id'])) {
        $newData['id'] = $params['id'];
    }

    $cleanInput = new \MicroweberPackages\Helper\HTMLClean();
    $newData = $cleanInput->cleanArray($newData);

    if (isset($params['tagging_tag_id']) && !empty($params['tagging_tag_id'])) {
        $tagging_tag_id = $params['tagging_tag_id'];
        $tag = db_get('tagging_tags', [
            'no_cache'=>false,
            'id'=>$tagging_tag_id,
            'single'=>1
        ]);
        if ($tag) {
            $newData['id'] = $tag['id'];
        }
    }

    if (empty($newData['slug'])) {
        $newData['slug'] = mw()->url_manager->slug($newData['name']);
    } else {
        $newData['slug'] = mw()->url_manager->slug($newData['slug']);
    }

    // Update all posts name with tag slug
    $getTaggingTagged = db_get('tagging_tagged', 'tag_slug='.$newData['slug'].'&no_cache=1');
    if ($getTaggingTagged) {
        foreach ($getTaggingTagged as $taggingTaggedPost) {

            $newTaggingTaggedPost = [];
            $newTaggingTaggedPost['id'] = $taggingTaggedPost['id'];
            $newTaggingTaggedPost['tag_name'] = $newData['name'];

            db_save('tagging_tagged', $newTaggingTaggedPost);
        }
    }

    if (!isset($newData['id'])) {
        $findTaggingTag = db_get('tagging_tags', 'slug=' . $newData['slug'].'&single=1');
        if ($findTaggingTag) {
            $newData['id'] = $findTaggingTag['id'];
            return ['status'=>false,'message'=>'The tag slug is allready exists.', 'id'=> $newData['id']];
        }
    }

    $tagSaved = db_save('tagging_tags',$newData);
    if ($tagSaved) {

        if (!isset($newData['id'])) {
            $newData['id'] = $tagSaved;
        }

        return $newData;
    }

    return ['status'=>false];

}
```

### Cross-File Context

[MicroweberPackages\Helper\HTMLClean — class — src/MicroweberPackages/Helper/HTMLClean.php:5-44]
class HTMLClean { public function cleanArray($array) { if (is_array($array)) { $cleanedArray = []; foreach ($array as $key=>$value) { $cleanedArray[$key] = $this->clean($value); } return $cleanedArray; } } public function clean($html) { $antiXss = new \voku\helper\AntiXSS(); $html = $antiXss->xss_clean($html); $path = storage_path() . '/html_purifier'; if (!is_dir($path)) { mkdir_recursive($path); } $config = \HTMLPurifier_Config::createDefault(); if ($path) { $config->set('Cache.SerializerPath', $path); } $config->set('URI.DisableExternal', true); $config->set('URI.DisableExternalResources', true); // $config->set('URI.DisableResources', true); $config->set('URI.Host', site_hostname()); $purifier = new \HTMLPurifier($config); $html = $purifier->purify($html); return $html; } }

[MicroweberPackages\Helper\HTMLClean::cleanArray — method — src/MicroweberPackages/Helper/HTMLClean.php:7-17]
public function cleanArray($array) { if (is_array($array)) { $cleanedArray = []; foreach ($array as $key=>$value) { $cleanedArray[$key] = $this->clean($value); } return $cleanedArray; } }

[MicroweberPackages\Helper\HTMLClean::clean — method — src/MicroweberPackages/Helper/HTMLClean.php:19-43]
public function clean($html) { $antiXss = new \voku\helper\AntiXSS(); $html = $antiXss->xss_clean($html); $path = storage_path() . '/html_purifier'; if (!is_dir($path)) { mkdir_recursive($path); } $config = \HTMLPurifier_Config::createDefault(); if ($path) { $config->set('Cache.SerializerPath', $path); } $config->set('URI.DisableExternal', true); $config->set('URI.DisableExternalResources', true); // $config->set('URI.DisableResources', true); $config->set('URI.Host', site_hostname()); $purifier = new \HTMLPurifier($config); $html = $purifier->purify($html); return $html; }

Control flow: The function tagging_tag_edit($params) receives user input and validates that the 'name' field is non-empty, returning early with status false if empty. It defaults 'slug' and 'description' to empty strings if not provided, then builds a $newData array from these parameters. The array is sanitized via HTMLClean::cleanArray(). If a tagging_tag_id is provided, it fetches the existing tag to validate the ID. The slug is then processed through mw()->url_manager->slug(). The function updates all related tagged posts with the new tag name, checks for slug uniqueness before insert, and calls db_save() to persist data. It returns the saved data on success or a status false array on failure.

Data flow: User-controlled input enters via $params['name'], $params['slug'], $params['description'], and $params['id']. These values are copied into $newData, which is then passed to cleanArray(). The cleanArray method iterates each value and applies the clean() method, which chains AntiXSS::xss_clean() followed by HTMLPurifier::purify() with configuration disabling external URIs and external resources. After sanitization, the slug undergoes additional processing via url_manager->slug(). The sanitized $newData is then passed to db_save() for database storage.

Verdict: Not Vulnerable

Reviewed region:
[userfiles/modules/tags/TaggingTagsManager.php]
```php
function tagging_tag_edit($params) {

    if (empty(trim($params['name']))) {
        return ['status'=>false];
    }

    if (!isset($params['slug'])) {
        $params['slug'] = '';
    }

    if (!isset($params['description'])) {
        $params['description'] = '';
    }

    $newData = [];
    $newData['name'] = $params['name'];
    $newData['slug'] = $params['slug'];
    $newData['description'] = $params['description'];
    if (isset($params['id'])) {
        $newData['id'] = $params['id'];
    }

    $cleanInput = new \MicroweberPackages\Helper\HTMLClean();
    $newData = $cleanInput->cleanArray($newData);

    if (isset($params['tagging_tag_id']) && !empty($params['tagging_tag_id'])) {
        $tagging_tag_id = $params['tagging_tag_id'];
        $tag = db_get('tagging_tags', [
            'no_cache'=>false,
            'id'=>$tagging_tag_id,
            'single'=>1
        ]);
        if ($tag) {
            $newData['id'] = $tag['id'];
        }
    }

    if (empty($newData['slug'])) {
        $newData['slug'] = mw()->url_manager->slug($newData['name']);
    } else {
        $newData['slug'] = mw()->url_manager->slug($newData['slug']);
    }

    // Update all posts name with tag slug
    $getTaggingTagged = db_get('tagging_tagged', 'tag_slug='.$newData['slug'].'&no_cache=1');
    if ($getTaggingTagged) {
        foreach ($getTaggingTagged as $taggingTaggedPost) {

            $newTaggingTaggedPost = [];
            $newTaggingTaggedPost['id'] = $taggingTaggedPost['id'];
            $newTaggingTaggedPost['tag_name'] = $newData['name'];

            db_save('tagging_tagged', $newTaggingTaggedPost);
        }
    }

    if (!isset($newData['id'])) {
        $findTaggingTag = db_get('tagging_tags', 'slug=' . $newData['slug'].'&single=1');
        if ($findTaggingTag) {
            $newData['id'] = $findTaggingTag['id'];
            return ['status'=>false,'message'=>'The tag slug is allready exists.', 'id'=> $newData['id']];
        }
    }

    $tagSaved = db_save('tagging_tags',$newData);
    if ($tagSaved) {

        if (!isset($newData['id'])) {
            $newData['id'] = $tagSaved;
        }

        return $newData;
    }

    return ['status'=>false];

}
```

Why it is not vulnerable: All user-controlled data stored in the database passes through a robust sanitization pipeline before reaching db_save(). The cleanArray() method iterates over every value in $newData and applies clean(), which uses both AntiXSS::xss_clean() and HTMLPurifier::purify(). HTMLPurifier is configured with URI.DisableExternal and URI.DisableExternalResources set to true, and URI.Host restricted to the site hostname. This multi-layered sanitization ensures that malicious content (XSS payloads, injection scripts, unsafe HTML) is stripped from all fields (name, slug, description, id) before database persistence.

Security invariants:
- All user input destined for database storage must pass through HTML sanitization before db_save() is called. Enforced by: $newData = $cleanInput->cleanArray($newData); which processes the entire data array before any database write.
- The clean() method must apply both XSS cleaning and HTML purification to each value. Enforced by: the clean() method first calling $antiXss->xss_clean($html) then $purifier->purify($html).
- External resources and URIs must be disabled in HTML purification to prevent injection via external content. Enforced by: $config->set('URI.DisableExternal', true); and $config->set('URI.DisableExternalResources', true); in the HTMLPurifier configuration.
- Empty or missing required fields must be rejected before processing. Enforced by: the early return if empty(trim($params['name'])).
- The slug field must be normalized through URL-safe processing. Enforced by: $newData['slug'] = mw()->url_manager->slug($newData['slug']); which runs after sanitization.

Invariant verification:
- All stored values are sanitized via cleanArray: holds=true. Evidence: The line $newData = $cleanInput->cleanArray($newData); is executed before db_save('tagging_tags',$newData), and cleanArray() iterates every key-value pair applying clean() to each value.
- HTMLPurifier configuration restricts external content injection: holds=true. Evidence: The clean() method sets $config->set('URI.DisableExternal', true);, $config->set('URI.DisableExternalResources', true);, and $config->set('URI.Host', site_hostname()); before purification.
- AntiXSS is applied as first sanitization layer: holds=true. Evidence: In clean(), the line $html = $antiXss->xss_clean($html); executes before HTMLPurifier processing, providing XSS payload removal as initial defense.
- Required input validation prevents empty data storage: holds=true. Evidence: The check if (empty(trim($params['name']))) { return ['status'=>false]; } prevents processing of invalid input at the entry point.

Residual risk notes: The db_get queries using string concatenation (e.g., 'tag_slug='.$newData['slug']) rely on the downstream database abstraction layer for SQL escaping rather than parameterized queries. However, since $newData['slug'] has already been sanitized and processed through url_manager->slug(), the risk is mitigated. The db_save() implementation details are not visible in the provided context, so the exact SQL parameterization mechanism cannot be independently verified, though the call path indicates it is part of the inspection scope. The tagging_tag_id parameter is used directly in db_get without sanitization, but it is passed in array format which typically enables parameterized queries.

**VULNERABLE: SQL Injection**

The code contains a SQL injection vulnerability in the database query that retrieves tagged posts.

**Vulnerable Code:**
```php
$getTaggingTagged = db_get('tagging_tagged', 'tag_slug='.$newData['slug'].'&no_cache=1');
```

**Root Cause:**
`$newData['slug']` is directly concatenated into the query string without parameterization or escaping. While the slug passes through `mw()->url_manager->slug()` for formatting and `HTMLClean::clean()` for XSS prevention, neither operation provides SQL injection protection.

**Data Flow Analysis:**
1. `$params['slug']` enters from user input
2. Passes through `HTMLClean::cleanArray()` → `HTMLClean::clean()`
3. `HTMLClean::clean()` applies AntiXSS and HTMLPurifier (XSS defenses, not SQL defenses)
4. Slug is formatted via `mw()->url_manager->slug()` (URL-friendly transformation, not SQL escaping)
5. Unsanitized slug concatenated directly into SQL query string

**Attack Vector:**
An attacker can inject SQL through the `slug` parameter:
```
POST /tag/edit
slug=' OR '1'='1' --
```

The resulting query becomes:
```
tag_slug=' OR '1'='1' -- &no_cache=1
```

This bypasses the intended filter and returns all records from `tagging_tagged`.

**Impact:**
- **Information Disclosure**: Retrieve all tagged posts regardless of slug
- **Data Manipulation**: Depending on `db_get` implementation, could enable UNION-based injection to extract arbitrary data
- **Privilege Escalation**: Access tags/posts the user shouldn't see

**Preconditions:**
- Attacker must reach `tagging_tag_edit` function (likely requires authentication based on context)
- `db_get` must use string concatenation internally (common in legacy PHP frameworks)

**Additional Vulnerable Query:**
```php
$findTaggingTag = db_get('tagging_tags', 'slug=' . $newData['slug'].'&single=1');
```
Same vulnerability pattern with identical root cause.

**Classification:** CWE-89 (SQL Injection)

**Mitigation:**
Use parameterized queries or proper escaping:
```php
$getTaggingTagged = db_get('tagging_tagged', [
    'tag_slug' => $newData['slug'],
    'no_cache' => 1
]);
```

```json
{
  "vulnerable": true,
  "cwe": "CWE-89",
  "severity": "HIGH",
  "reviewed_region": "userfiles/modules/tags/TaggingTagsManager.php:tagging_tag_edit",
  "vulnerable_region": "Line with db_get('tagging_tagged', 'tag_slug='.$newData['slug'].'&no_cache=1') and db_get('tagging_tags', 'slug=' . $newData['slug'].'&single=1')",
  "root_cause": "User-controlled slug value is directly concatenated into query string parameter without SQL escaping or parameterization. HTMLClean::clean() applies XSS defenses (AntiXSS, HTMLPurifier) but provides no SQL injection protection. The url_manager->slug() function performs URL-friendly formatting, not SQL sanitization.",
  "exploitability": "HIGH - Attacker can inject SQL metacharacters through the slug parameter. The cleaning pipeline (HTMLClean + url_manager->slug) does not neutralize SQL injection payloads. String concatenation pattern 'tag_slug='.$newData['slug'] allows direct injection into the query string passed to db_get().",
  "attack_preconditions": "Attacker must have access to invoke tagging_tag_edit function (likely requires authenticated access based on module context). The db_get() implementation must use string-based query construction vulnerable to injection (common pattern in legacy PHP frameworks).",
  "impact": "SQL Injection enables: (1) Bypassing tag_slug filter to retrieve all tagging_tagged records regardless of intended scope, (2) Potential UNION-based injection to extract arbitrary database contents depending on db_get implementation, (3) Information disclosure of tags and associated post relationships, (4) Possible data manipulation if db_get supports write operations through injection.",
  "non_vulnerable_reason": null,
  "security_invariants": null,
  "invariant_verification": null,
  "residual_risk_notes": "Actual exploitability depends on db_get() implementation details not shown in context. If db_get() uses parameterized queries internally despite string parameter interface, vulnerability may not be exploitable. However, the string concatenation pattern strongly suggests vulnerable implementation. The second vulnerable query (findTaggingTag) has identical root cause.",
  "call_path": "tagging_tag_edit → db_get (via string concatenation with unsanitized slug)"
}
```