--- a/sys/vfs/hammer2/hammer2.h 2026-08-29 07:17:55.442213003 +0000 +++ b/sys/vfs/hammer2/hammer2.h 2026-08-29 07:19:39.972891167 +0000 @@ -707,6 +707,15 @@ uint8_t comp_heuristic; hammer2_inode_meta_t meta; /* copy of meta-data */ hammer2_off_t osize; + /* + * DF-2633: asynchronous backend error state (errno). Backend + * paths that cannot return an error to the syscall which + * started the work (strategy xop allocation failures, syncer + * inode topology insertions) store the errno here so that + * write(2) and fsync(2) can report it, instead of silently + * succeeding while data is being lost. Accessed atomically. + */ + int error; }; typedef struct hammer2_inode hammer2_inode_t; @@ -1603,7 +1612,7 @@ hammer2_blockref_t *base, int count, hammer2_chain_t *chain, hammer2_blockref_t *obref); -void hammer2_base_insert(hammer2_chain_t *parent, +int hammer2_base_insert(hammer2_chain_t *parent, hammer2_blockref_t *base, int count, hammer2_chain_t *chain, hammer2_blockref_t *elm); --- a/sys/vfs/hammer2/hammer2_chain.c 2026-08-29 07:17:36.418453561 +0000 +++ b/sys/vfs/hammer2/hammer2_chain.c 2026-08-29 07:19:30.969005026 +0000 @@ -3489,6 +3489,8 @@ hammer2_tid_t mtid, int flags, hammer2_blockref_t *obref) { + int error; + hammer2_chain_rename(parentp, chain, mtid, flags); if (obref->type != HAMMER2_BREF_TYPE_EMPTY) { @@ -3498,7 +3500,27 @@ KKASSERT((chain->flags & HAMMER2_CHAIN_BLKMAPPED) == 0); hammer2_chain_modify(*parentp, mtid, 0, 0); tbase = hammer2_chain_base_and_count(*parentp, &tcount); - hammer2_base_insert(*parentp, tbase, tcount, chain, obref); + error = hammer2_base_insert(*parentp, tbase, tcount, chain, + obref); + if (error) { + /* + * DF-2632: inserting the preserved obref overlaps an + * element already present in the target blockref + * array. Observed from the flusher's indirect- + * maintenance collapse path when directory-entry + * keys are densely packed into one dirhash window. + * + * Skipping the media insert is safe: the chain is + * already in the new parent's RBTREE. Set + * UPDATE|BLKMAPUPD so a later flush retries the + * blockref insertion -- i.e. the flush defers + * instead of panicking the kernel. + */ + atomic_set_int(&chain->flags, + HAMMER2_CHAIN_BLKMAPUPD | + HAMMER2_CHAIN_UPDATE); + return; + } if (bcmp(obref, &chain->bref, sizeof(chain->bref))) { atomic_set_int(&chain->flags, HAMMER2_CHAIN_BLKMAPUPD | HAMMER2_CHAIN_UPDATE); @@ -5218,8 +5240,15 @@ * * NOTE: live_count was adjusted when the chain was deleted, so it does not * need to be adjusted when we commit the media change. + * + * DF-2632: Returns 0 on success or an error if the element would overlap + * an existing element in the array. In that case the insert is skipped + * (the array is left unchanged) and the caller is expected to defer the + * operation (e.g. set HAMMER2_CHAIN_UPDATE so a later flush retries), + * instead of the previous unconditional panic which made a dense + * directory-entry window a remote kernel-panic DoS. */ -void +int hammer2_base_insert(hammer2_chain_t *parent, hammer2_blockref_t *base, int count, hammer2_chain_t *chain, hammer2_blockref_t *elm) @@ -5231,6 +5260,7 @@ int k; int l; int u = 1; + int overlap; /* * Insert new element. Expect the element to not already exist @@ -5250,6 +5280,40 @@ KKASSERT(i >= 0 && i <= count); /* + * DF-2632: check the overlap condition BEFORE any stats side + * effects so a skipped insert leaves the parent fully consistent. + * The append shortcut below (i == count) cannot overlap by + * construction. + */ + xkey = elm->key + ((hammer2_key_t)1 << elm->keybits) - 1; + overlap = (i != count && + (base[i].key < elm->key || xkey >= base[i].key)); + if (overlap) { + /* + * Do NOT touch the spinlock here: some callers + * (hammer2_chain_rename_obref from indirect maintenance) + * do not hold parent->core.spin; the old panic path's + * unconditional hammer2_spin_unex() corrupted the + * critcount in exactly that case (the INVARIANTS + * "td_critcount is/would-go negative" panic). + * + * kprintf is spin-safe. + */ + if (chain) { + atomic_clear_int(&chain->flags, + HAMMER2_CHAIN_BLKMAPPED); + } + krateprintf(&krate_h2me, + "hammer2: base_insert overlap deferred: " + "parent %p type %d elm %016jx/%d type %d " + "vs base[%d] %016jx/%d type %d\n", + parent, parent->bref.type, + elm->key, elm->keybits, elm->type, + i, base[i].key, base[i].keybits, base[i].type); + return (HAMMER2_ERROR_EINVAL); + } + + /* * Set appropriate blockmap flags in chain (if not NULL) */ if (chain) @@ -5301,14 +5365,7 @@ if (i == count && parent->core.live_zero < count) { i = parent->core.live_zero++; base[i] = *elm; - return; - } - - xkey = elm->key + ((hammer2_key_t)1 << elm->keybits) - 1; - if (i != count && (base[i].key < elm->key || xkey >= base[i].key)) { - hammer2_spin_unex(&parent->core.spin); - panic("insert base %p overlapping elements at %d elm %p\n", - base, i, elm); + return(0); } /* @@ -5368,6 +5425,7 @@ } } + return(0); } --- a/sys/vfs/hammer2/hammer2_flush.c 2026-08-29 07:17:36.422453511 +0000 +++ b/sys/vfs/hammer2/hammer2_flush.c 2026-08-29 07:19:07.225305277 +0000 @@ -487,6 +487,7 @@ hammer2_chain_t *parent; hammer2_dev_t *hmp; int save_error; + int error; int retry; retry = 0; @@ -1127,8 +1128,21 @@ } if (base && (chain->flags & HAMMER2_CHAIN_BLKMAPPED) == 0) { hammer2_spin_ex(&parent->core.spin); - hammer2_base_insert(parent, base, count, - chain, &chain->bref); + error = hammer2_base_insert(parent, base, count, + chain, &chain->bref); + if (error) { + /* + * DF-2632: overlap detected while updating + * the parent's blockref array. Defer: keep + * the UPDATE flag set so a later flush + * retries the insertion instead of the + * previous unconditional panic. + */ + atomic_set_int(&chain->flags, + HAMMER2_CHAIN_UPDATE); + info->error |= error; + retry = 1; + } hammer2_spin_unex(&parent->core.spin); /* base_insert sets BLKMAPPED */ } --- a/sys/vfs/hammer2/hammer2_inode.c 2026-08-29 07:17:36.426453460 +0000 +++ b/sys/vfs/hammer2/hammer2_inode.c 2026-08-29 07:19:58.072662285 +0000 @@ -42,6 +42,8 @@ #include "hammer2.h" +static struct krate krate_h2err = { .freq = 1 }; + #define INODE_DEBUG 0 /* @@ -1723,13 +1725,16 @@ if (error == HAMMER2_ERROR_ENOENT) error = 0; if (error) { - kprintf("hammer2: unable to fsync inode %p\n", ip); /* - atomic_set_int(&ip->flags, - xop->ipflags & (HAMMER2_INODE_RESIZED | - HAMMER2_INODE_MODIFIED)); - */ - /* XXX return error somehow? */ + * DF-2633: record the fsync-time inode sync + * failure so the next write(2)/fsync(2) reports + * it; rate-limit the console output. + */ + atomic_swap_int(&ip->error, + hammer2_error_to_errno(error)); + krateprintf(&krate_h2err, + "hammer2: unable to fsync inode %p: %s\n", + ip, hammer2_error_str(error)); } } return error; @@ -1758,9 +1763,22 @@ if (error == HAMMER2_ERROR_ENOENT) error = 0; if (error) { - kprintf("hammer2: backend unable to " - "insert inode %p %ld\n", ip, (long)ip->meta.inum); - /* XXX return error somehow? */ + /* + * DF-2633: the file appeared to be created + * successfully (the dirent is live, the data may + * even be flushed), but its inode never made it + * into the on-media topology. Record the failure + * on the inode so write(2)/fsync(2) report it + * instead of silently succeeding; rate-limit the + * console flood. + */ + atomic_swap_int(&ip->error, + hammer2_error_to_errno(error)); + krateprintf(&krate_h2err, + "hammer2: backend unable to " + "insert inode %p %ld: %s\n", + ip, (long)ip->meta.inum, + hammer2_error_str(error)); } } return error; --- a/sys/vfs/hammer2/hammer2_strategy.c 2026-08-29 07:17:36.430453409 +0000 +++ b/sys/vfs/hammer2/hammer2_strategy.c 2026-08-29 07:20:17.440417367 +0000 @@ -53,6 +53,8 @@ #include #include "hammer2.h" + +static struct krate krate_h2err = { .freq = 1 }; #include "hammer2_lz4.h" #include "zlib/hammer2_zlib.h" @@ -673,10 +675,21 @@ bp->b_error = 0; biodone(bio); } else { - kprintf("xop_strategy_write: error %d loff=%016jx\n", - error, (intmax_t)bp->b_loffset); + /* + * DF-2633: the backend write failed (typically a + * freemap allocation failure on a full PFS) after the + * frontend write(2) already returned success. Record + * the errno on the inode so the next write(2)/fsync(2) + * on this file reports the failure instead of losing + * the data silently. Rate-limit the console flood. + */ + atomic_swap_int(&ip->error, + hammer2_error_to_errno(error)); + krateprintf(&krate_h2err, + "xop_strategy_write: error %d loff=%016jx\n", + error, (intmax_t)bp->b_loffset); bp->b_flags |= B_ERROR; - bp->b_error = EIO; + bp->b_error = hammer2_error_to_errno(error); biodone(bio); } hammer2_xop_retire(&xop->head, HAMMER2_XOPMASK_VOP); --- a/sys/vfs/hammer2/hammer2_vnops.c 2026-08-29 07:17:36.430453409 +0000 +++ b/sys/vfs/hammer2/hammer2_vnops.c 2026-08-29 07:20:52.503973963 +0000 @@ -58,6 +58,8 @@ #include "hammer2.h" +static struct krate krate_h2err = { .freq = 1 }; + static int hammer2_read_file(hammer2_inode_t *ip, struct uio *uio, int seqcount); static int hammer2_write_file(hammer2_inode_t *ip, struct uio *uio, @@ -272,6 +274,18 @@ hammer2_inode_unlock(ip); hammer2_trans_done(ip->pmp, 0); + /* + * DF-2633: fsync(2) must report asynchronous backend write + * failures recorded on this inode (strategy allocation + * failures, topology insert failures). POSIX requires fsync + * to report I/O errors on the data it claims to have flushed; + * previously these were dropped and fsync returned 0 while + * the data was lost. + */ + error2 = atomic_swap_int(&ip->error, 0); + if (error2) + error1 = error2; + return (error1); } @@ -851,6 +865,22 @@ error = 0; if (ip->pmp->ronly || (ip->pmp->flags & HAMMER2_PMPF_EMERG)) return (EROFS); + + /* + * DF-2633: report a deferred backend failure (async write + * allocation failure, inode topology insert failure) recorded + * by the strategy xop or the syncer. Without this the writer + * keeps seeing write(2) succeed while the data is being lost. + * + * (VM-pager UIO_NOCOPY writes are excluded; the pager cannot + * meaningfully act on a deferred errno here.) + */ + if (uio->uio_segflg != UIO_NOCOPY) { + error = atomic_swap_int(&ip->error, 0); + if (error) + return (error); + } + switch (hammer2_vfs_enospace(ip, uio->uio_resid, ap->a_cred)) { case 2: return (ENOSPC);