The error message hd0,X not found comes from GRUB, and indicates GRUB configuration file has an error, pointing to a partition that either does not exist or is not readable by GRUB.
The titular error message Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) means you got through GRUB and into the Linux kernel, but the kernel cannot find the device specified for the root filesystem, i.e. either you are missing a driver that should have been included into your kernel (or initramfs, if you used that... but LFS doesn't), or your root=/dev/... kernel boot option is not correct.
More specifically, unknown-block(0,0) means the kernel has no clue what block device your root filesystem is supposed to be on; unknown-block(8,1) indicates you specified /dev/sda1 and the kernel understood that but has not been able to find such a partition. The numbers refer to the major & minor numbers of the device node:
ls -l /dev/sda1
brw-rw---- 1 root disk 8, 1 May 4 07:56 /dev/sda1
^^^^^
The first mistake would be here:
cfdisk linux.img
mkfs.ext4 linux.img
This suggests you first created a partition table (with an unspecified number of partitions of unknown types) and then overwrote the partition table by initializing the entire image as a single ext4 filesystem.
As a result, there will be no partitions at all: in GRUB terminology, there will be no (hd0,0) nor (hd0,1) nor (hd0,2), only just (hd0).
There also won't be any unallocated space between the MBR partition table and the beginning of the first partition to embed the core image of GRUB into, so you may have seen some warnings when installing GRUB. It is possible that the installer could have adapted to the situation by falling back to an old installation style that is now seriously discouraged.
For creating a whole-disk image, I would recommended the following procedure instead:
dd if=/dev/zero of=linux.img bs=16M count=1024
cfdisk linux.img
losetup -P /dev/loop0 linux.img
ls /dev/loop0* #you should see loop0 and the first partition as loop0p1
mkfs.ext4 /dev/loop0pX # replace X with partition number
mkswap /dev/loop0pY # if you created a swap partition, use this for it and replace Y
... # LFS 11.3 chapter 2.6 and the 1st command of chapter 2.7
mount -v -t ext4 /dev/loop0pX $LFS # the 2nd command of LFS 11.3 chapter 2.7
... # continue according to the book
Now, in the grub.cfg file, the set root= entry should be of the form (hd0,<X-1>) because Linux starts counting partitions from 1, but GRUB counts them starting from 0. So if you used /dev/loop0p1 for your ext4 partition, use set root=(hd0,0) for GRUB.
Also, on the linux line, the root=/dev/hda1 refers to the old deprecated non-libata drivers for the archaic PATA disk controllers. Most modern Linux disk controller drivers now use the /dev/sdXN style names. So even though you may have used /dev/loop0p1 to create the ext4 filesystem of your LFS image, once that image is presented as a virtual hard disk within the QEMU VM, the kernel will name it /dev/sda1 unless you made some strange choices in kernel configuration and used different QEMU options. So the last line of your grub.cfg should be:
linux /boot/vmlinuz-6.1.11-lfs-11.3 root=/dev/sda1 ro
(For your current, mis-prepared linux.img, you could try root=/dev/sda instead, just for fun; it might or might not work.)