[Magisk] TheFreeman193:
For those unsure of how to find the offset for @badabing2003's kernel release string patch script, I've put together a script that should find the offset in a kernel file, boot image, or boot partition.

It finds matches for uname -r and checks the surrounding bytes for valid sysname (uname -s) or machine (uname -m) UTS fields matching the current values from uname.

#!/system/bin/sh
# Copyright (C) MIT License 2024 Nicholas Bissell (TheFreeman193)

NL="
"
SYSNMLN=65

showHeader() {
echo "

========= Kernel release offset finder =========
Buy me a coffee: https://ko-fi.com/nickbissell
===================== v2.0 =====================
" >&2
}

showUsage() {
showHeader 2>&1
echo "Usage:
$0 --auto [--hex] [--quiet] [--patch new_value [--inplace]]
$0 [--kernel] kernel_file [--hex] [--quiet] [--patch new_value [--inplace]]
$0 --image boot_img [--hex] [--quiet] [--patch new_value [--inplace]]
$0 --boot boot_part [--hex] [--quiet] [--patch new_value [--inplace]]

--auto -a Detect boot partition and extract automatically
--kernel -k Get offset from an uncompressed kernel file (e.g. kernel)
--image -i Get offset from a boot image file (e.g. boot.img)
--boot -b Get offset from a boot partition (e.g. /dev/block/...)
--hex -x Return offset in hexadecimal format
--quiet -q Quiet operation - don't print operations to stderr
--patch -p Patch the partition/image/kernel with new_value
--inplace -n For --kernel/--image: Patch the original file instead of copying
kernel_file An uncompressed Linux kernel extracted with magiskboot
boot_img A boot image file containing the Linux kernel
boot_part A path to the boot partition of the device
new_value A new value for the non-numeric part of the release string
"
exit 0
}

[ $# -eq 0 ] || [ -z "$1" ] && showUsage

if [ ! -f /data/adb/magisk/busybox ] || [ ! -f /data/adb/magisk/magiskboot ]; then
echo "ERROR: Critical Magisk components not found. Make sure magisk is installed and this is a root shell." >&2
exit 2
fi

for cmd in dd xxd strings stat sed grep; do
alias $cmd="/data/adb/magisk/busybox $cmd"
done

readUtsFieldHex() {
[ -z "$SYSNMLN" ] && SYSNMLN=65
dd if="$1" skip="$2" count="$SYSNMLN" iflag="skip_bytes,count_bytes" status=none | xxd -p -c "$SYSNMLN"
}

toUtsFieldHex() {
[ -z "$SYSNMLN" ] && SYSNMLN=65
hexSYSNMLN="$((SYSNMLN * 2))"
hexPad="$(dd if=/dev/zero iflag=count_bytes count="$SYSNMLN" status=none | xxd -p -c "$SYSNMLN")"
echo "$(printf %s "$1" | xxd -p -c "$SYSNMLN")$hexPad" | cut -b "-$hexSYSNMLN"
}

findStringsInBin() {
strings -t d "$1" | grep -F "$2" | sed -r 's/^[[:space:]]*([0-9]+)[[:space:]]+.+$/\1/g' | grep -E "^[0-9]+$"
}

findUtsFieldOffset() {
file="$1"
value="$2"
[ -z "$SYSNMLN" ] && SYSNMLN=65
[ $quietMode -eq 0 ] && echo "${NL}Looking for instances of '$value' in '$file'..." >&2
curSys="$(uname -s | cut -b "-$SYSNMLN")"
curMachine="$(uname -m | cut -b "-$SYSNMLN")"
hexValue="$(toUtsFieldHex "$value")"
hexCurSys="$(toUtsFieldHex "$curSys")"
hexCurMachine="$(toUtsFieldHex "$curMachine")"
potentialOffsets="$(findStringsInBin "$file" "$value")"
validOffsets=""
[ $asHex -eq 0 ] && offsetFormat="%s%s... " || offsetFormat="%s0x%x... "
for offset in $potentialOffsets; do
[ $quietMode -eq 0 ] && printf "$offsetFormat" " Checking match at offset " "$offset" >&2
if [ "$(readUtsFieldHex "$file" "$offset")" == "$hexValue" ]; then
found=""
sysOffset="$((offset - SYSNMLN * 5))"
endOffset="$((offset + SYSNMLN * 5))"
while [ "$sysOffset" -lt "$endOffset" ]; do
curHex="$(readUtsFieldHex "$file" "$sysOffset")"
if [ "$curHex" == "$hexCurSys" ]; then
found="sysname"
break 1
elif [ "$curHex" == "$hexCurMachine" ]; then
found="machine"
break 1
fi
sysOffset="$((sysOffset + SYSNMLN))"
done
if [ -n "$found" ]; then
validOffsets+="$offset$NL"
[ $quietMode -eq 0 ] && echo "Found UTS $found field!" >&2
else
[ $quietMode -eq 0 ] && echo "Not a complete UTS structure" >&2
fi
else
[ $quietMode -eq 0 ] && echo "Partial match/not UTS" >&2
fi
done
if [ -n "$validOffsets" ]; then
echo "$validOffsets" | sed '/^$/d'
else
echo "${NL}No offsets with UTS format found for '$curRelease'" >&2
return 1
fi
}

findBootPath() {
[ $quietMode -eq 0 ] && echo "${NL}Detecting boot partition path..." >&2

bootPath="/dev/block/bootdevice/by-name"
if [ ! -d "$bootPath/" ]; then
echo "ERROR: Boot partition directory not found." >&2
return 1
fi

if [ -e "$bootPath/boot" ]; then
echo "$bootPath/boot"
elif [ -e "$bootPath/boot_a" ] && [ -e "$bootPath/boot_b" ]; then
slot_suffix="$(getprop ro.boot.slot_suffix)"
if [ -n "$slot_suffix" ]; then
echo "$bootPath/boot$slot_suffix"
return 0
fi
current_slot="$(getprop current-slot)"
if [ -n "$current_slot" ]; then
echo "$bootPath/boot$current_slot"
return 0
fi
command -v bootctl && slot_number="$(bootctl get-current-slot)"
[ -n "$slot_number" ] && slot_suffix="$(bootctl get-suffix "$slot_number")"
if [ -n "$slot_suffix" ]; then
echo "$bootPath/boot$slot_suffix"
return 0
fi
slot_suffix="$(cat /proc/cmdline | sed 's/ /\n/g' | grep -Ei 'slot_suffix|current-slot' | sed -r 's/.+=//')"
if [ -n "$slot_suffix" ]; then
echo "$bootPath/boot$slot_suffix"
return 0
fi
echo "ERROR: Cannot get active slot." >&2
return 1
else
echo "ERROR: Unexpected boot partition configuration." >&2
return 1
fi
}

getOutFile() {
fileOut="$1.new"
ctr=1
while [ -f "$fileOut" ]; do
ctr=$((ctr + 1))
fileOut="$1.new$ctr"
done
echo "$fileOut"
}

patchKernel() {
if [ $# -ne 3 ]; then
echo "ERROR patchKernel: Invalid number of parameters!" >&2
return 21
fi
if [ ! -f "$1" ]; then
echo "ERROR patchKernel: Kernel file doesn't exist!" >&2
return 22
fi
if [ -z "$2" ]; then
echo "ERROR patchKernel: Replacement string is empty!" >&2
return 23
fi
if [ -z "$3" ]; then
echo "ERROR patchKernel: No replacement offsets provided!" >&2
return 24
fi
kSize="$(stat -c "%s" $1)"
maxOffset=$((kSize - SYSNMLN))
maxLen=$((SYSNMLN - 1))
[ $quietMode -eq 0 ] && echo "${NL}Patching kernel file '$1'..." >&2
for offset in $3; do
if [ "$offset" -lt 1 ] || [ "$offset" -gt $maxOffset ]; then
echo "WARNING patchKernel: Offset $offset not between 0 and $maxOffset. Ignoring." >&2
continue
fi
if [ $quietMode -eq 0 ]; then
[ $asHex -eq 1 ] && prettyOffset="$(printf "0x%x" "$offset")" || prettyOffset="$offset"
printf %s " Offset $prettyOffset: " >&2
fi
curVal="$(dd if="$1" iflag="count_bytes,skip_bytes" skip="$offset" count="$SYSNMLN" status=none)"
versionPart="$(echo "$curVal" | sed -r 's/^([0-9.]+).*/\1/g')"
replRaw="$versionPart$2"
replSafe="${replRaw:0:$maxLen}"
dd if="/dev/zero" conv=notrunc iflag=count_bytes count="$SYSNMLN" oflag=seek_bytes seek="$offset" of="$1" status=none 2>/dev/null
printf %s "$replSafe" | dd conv=notrunc oflag="seek_bytes" seek="$offset" of="$1" status=none 2>/dev/null
readBack="$(dd if="$1" iflag="count_bytes,skip_bytes" skip="$offset" count="$SYSNMLN" status=none)"
if [ "$readBack" == "$replSafe" ]; then
[ $quietMode -eq 0 ] && echo "'$curVal' -> '$replSafe'" >&2
else
echo "ERROR patchKernel: Read back '$readBack' doesn't match target value '$replSafe'!" >&2
return 25
fi
[ "${#replRaw}" -gt $maxLen ] && echo "WARNING patchKernel: Replacement string is longer than field length ($maxLen) and has been truncated." >&2
done
return 0
}

newStage() {
lastDir="$PWD"
stageDir="/data/local/tmp/kr_offset_$RANDOM"
mkdir "$stageDir"
if [ ! -d "$stageDir" ]; then
echo "ERROR: Couldn't create temporary directory '$stageDir'" >&2
return 1
fi
cd "$stageDir"
}

cleanupQuit() {
if [ -n "$stageDir" ] && [ -d "$stageDir" ]; then
[ -n "$lastDir" ] && [ -d "$lastDir" ] && cd "$lastDir"
rm -rf "$stageDir"
fi
[ -n "$1" ] && exit $1 || exit 0
}

kernelFile=""
imageFile=""
bootPath=""
newValue=""
autoExtract=0
asHex=0
quietMode=0
inPlace=0
while [ $# -gt 0 ]; do
param="$1"
hasShifted=0
case "$param" in
-h|-\?|--help)
showUsage
;;
--kernel|-k|-[aqxn]*k)
kernelFile="$(readlink -f "$2")"
shift; shift; hasShifted=1
;;
--image|-i|-[aqxn]*i)
imageFile="$(readlink -f "$2")"
shift; shift; hasShifted=1
;;
--boot|-b|-[aqxn]*b)
bootPath="$2"
shift; shift; hasShifted=1
;;
--patch|-p|-[aqxn]*p)
newValue="$2"
shift; shift; hasShifted=1
;;
--auto|-a)
autoExtract=1
shift; continue
;;
--quiet|-q)
quietMode=1
shift; continue
;;
--hex|-x)
asHex=1
shift; continue
;;
--inplace|-n)
inPlace=1
shift; continue
;;
*)
if echo "$1" | grep -Eqv '^-'; then
kernelFile="$(readlink -f "$1")"
shift; continue
fi
;;
esac
echo "$param" | grep -Eq '^-[qxn]*a[qxn]*[kibp]$' && autoExtract=1
echo "$param" | grep -Eq '^-[qan]*x[qan]*[kibp]$' && asHex=1
echo "$param" | grep -Eq '^-[axn]*q[axn]*[kibp]$' && quietMode=1
echo "$param" | grep -Eq '^-[qax]*n[qax]*[kibp]$' && inPlace=1
[ $hasShifted -eq 1 ] && continue
echo "ERROR: Couldn't parse parameter '$1'." >&2
exit 1
done

[ $quietMode -eq 0 ] && showHeader

primArgs=0
mode=0
[ -n "$kernelFile" ] && mode=1 && primArgs=$((primArgs + 1))
[ -n "$imageFile" ] && mode=2 && primArgs=$((primArgs + 1))
[ -n "$bootPath" ] && mode=3 && primArgs=$((primArgs + 1))
[ $autoExtract -eq 1 ] && mode=4 && primArgs=$((primArgs + 1))

if [ $primArgs -gt 1 ]; then
echo "ERROR: Too many parameters ($primArgs) - only 1 of --kernel, --image, --boot, --auto allowed." >&2
exit 3
fi

if [ $mode -eq 0 ]; then
echo "ERROR: You must either pass a kernel file, boot image, partition path, or use --auto." >&2
exit 4
fi

if [ $mode -ge 4 ]; then
bootPath="$(findBootPath)" || cleanupQuit 5
fi

if [ $mode -ge 2 ]; then
newStage
[ $? -ne 0 ] && cleanupQuit 6
fi

if [ $mode -ge 3 ]; then
if [ ! -e "$bootPath" ]; then
echo "ERROR: Boot partition path '$bootPath' not found." >&2
cleanupQuit 7
fi
imageFile="boot.img"
[ $quietMode -eq 0 ] && echo "${NL}Extracting boot image '$bootPath'..." >&2
dd if="$bootPath" of="$imageFile" status=none
if [ ! -f "$imageFile" ]; then
echo "ERROR: Failed to extract boot image." >&2
cleanupQuit 8
fi
fi

if [ $mode -ge 2 ]; then
kernelFile="kernel"
[ $quietMode -eq 0 ] && echo "${NL}Unpacking boot image '$imageFile'..." >&2
/data/adb/magisk/magiskboot unpack "$imageFile" 2>/dev/null
if [ $? -ne 0 ] || [ ! -f "$kernelFile" ]; then
echo "ERROR: Failed to unpack boot image." >&2
cleanupQuit 9
fi
fi

if [ $mode -eq 1 ] && [ ! -f "$kernelFile" ]; then
echo "ERROR: Failed to unpack boot image." >&2
cleanupQuit 10
fi

curRelease="$(uname -r | cut -b "-$SYSNMLN")"
releaseOffsets="$(findUtsFieldOffset "$kernelFile" "$curRelease")"

[ $? -ne 0 ] || [ -z "$releaseOffsets" ] && cleanupQuit 0

if [ -z "$newValue" ]; then
[ $quietMode -eq 0 ] && echo "" >&2
if [ $asHex -eq 1 ]; then
for offset in $releaseOffsets; do
printf "0x%x\n" "$offset"
done
else
echo "$releaseOffsets"
fi
[ $quietMode -eq 0 ] && echo "" >&2
cleanupQuit 0
fi

if [ $mode -eq 1 ]; then
if [ $inPlace -eq 1 ]; then
kernelOut="$kernelFile"
else
kernelOut="$(getOutFile "$kernelFile")"
dd if="$kernelFile" of="$kernelOut" status=none
fi
patchKernel "$kernelOut" "$newValue" "$releaseOffsets"
res=$?; [ $res -ne 0 ] && cleanupQuit $res
echo "$kernelOut"
cleanupQuit 0
fi

patchKernel "$kernelFile" "$newValue" "$releaseOffsets"
res=$?; [ $res -ne 0 ] && cleanupQuit $res

imageOut="$(getOutFile "$imageFile")"
[ $quietMode -eq 0 ] && echo "${NL}Packing new boot image '$imageOut'..." >&2
/data/adb/magisk/magiskboot repack "$imageFile" "$imageOut" 2>/dev/null
if [ $? -ne 0 ] || [ ! -f "$imageOut" ]; then
cleanupQuit 11
fi

if [ $mode -eq 2 ]; then
if [ $inPlace -eq 1 ]; then
[ $quietMode -eq 0 ] && echo "${NL}Overwriting existing image '$imageFile'..." >&2
mv -f "$imageOut" "$imageFile"
echo "$imageFile"
else
echo "$imageOut"
fi
cleanupQuit 0
fi

[ $quietMode -eq 0 ] && echo "${NL}Flashing new boot image to '$bootPath'..." >&2
srcHash="$(sha1sum -b "$imageOut")"
dd if="$imageOut" of="$bootPath" status=none
[ $? -ne 0 ] && cleanupQuit 12
destHash="$(sha1sum -b "$bootPath")"
if [ "$destHash" != "$srcHash" ]; then
echo "ERROR: SHA-1 hash of flashed image '$bootPath' doesn't match source '$imageOut'!" >&2
cleanupQuit 13
fi

[ $quietMode -eq 0 ] && echo "${NL}Finished. Patched '$bootPath'.$NL" >&2
echo "$bootPath"
[ $quietMode -eq 0 ] && echo "" >&2
cleanupQuit 0

It's also available as a GitHub Gist.

In most cases, you should be able to run:

./kr_offset.sh --auto

And get an offset based on the kernel in the current boot image (boot slot detection for A/B devices adapted from @badabing2003's code).

The script also supports patching the kernel/image/partition with a new value after the numeric version part of the release string. The script prevents you from changing the numeric part or writing past the end of the release field, and permits changing the length of the release string (within the overall field length, currently 64 chars).

uname -r
# 4.19.230-lineageos
./kr_offset.sh --auto --patch "-newvalue"

Detecting boot partition path...

Extracting boot image '/dev/block/bootdevice/by-name/boot'...

Unpacking boot image 'boot.img'...

Looking for instances of '4.19.230-lineageos' in 'kernel'...
Checking match at offset 18623166... Partial match/not UTS
Checking match at offset 20246512... Partial match/not UTS
Checking match at offset 25785880... Partial match/not UTS
Checking match at offset 38518704... Found UTS sysname field!

Patching kernel file 'kernel'...
Offset 38518704: '4.19.230-lineageos' -> '4.19.230-newvalue'

Packing new boot image 'boot.img.new'...

Flashing new boot image to '/dev/block/bootdevice/by-name/boot'...

Finished. Patched '/dev/block/bootdevice/by-name/boot'.

/dev/block/bootdevice/by-name/boot

In this mode, the script returns the path of the parition/image/kernel file that was patched. In this case, that's the auto-detected boot partition. After rebooting:

uname -r
# 4.19.230-newvalue

If you're an advanced user and don't need the protections included in this script, I recommend @badabing2003's kernel release string patch script. Please note if using other patching scripts that the offset returned is the START of kernel release string. When using a different patch script, you should either repeat the numeric version part from uname -r or add to the offset so that you don't change/overwrite the kernel version part. For example:

uname -r
# 4.19.230-lineageos
./kr_offset.sh --auto --quiet
# 38518704

The offset returned in the example (38518704) is for the 4 of 4.19.230-lineageos. You could therefore run Bing's patch script as either:

# kr_patch.sh is Bing's patch script in this example
./kr_patch.sh -offset 38518704 "4.19.230-somethingelse"
# or
./kr_patch.sh -offset 38518713 "somethingelse"

Note how the offset in the second command has been adjusted to skip past the kernel version part.

Full usage:

./kr_offset.sh --auto [--hex] [--quiet] [--patch new_value [--inplace]]
./kr_offset.sh [--kernel] kernel_file [--hex] [--quiet] [--patch new_value [--inplace]]
./kr_offset.sh --image boot_img [--hex] [--quiet] [--patch new_value [--inplace]]
./kr_offset.sh --boot boot_part [--hex] [--quiet] [--patch new_value [--inplace]]

--auto -a Detect boot partition and extract automatically
--kernel -k Get offset from an uncompressed kernel file (e.g. kernel)
--image -i Get offset from a boot image file (e.g. boot.img)
--boot -b Get offset from a boot partition (e.g. /dev/block/...)
--hex -x Return offset in hexadecimal format
--quiet -q Quiet operation - don't print operations to stderr
--patch -p Patch the partition/image/kernel with new_value
--inplace -n For --kernel/--image: Patch the original file instead of copying
kernel_file An uncompressed Linux kernel extracted with magiskboot
boot_img A boot image file containing the Linux kernel
boot_part A path to the boot partition of the device
new_value A new value for the non-numeric part of the release string

If you have a kernel file already and don't want any bells and whistles, there is an earlier development version here.

Edit: Fix error for relative --boot image paths.

Edit 2: Updated to include patching support with --patch.