Guide to bootloader and bricking [2025] -- Discussion

Sure, use whatever you like for your guide :slight_smile:

Regarding vbmeta partition: I pointed it out because i was curious if there was a ‘non-deleteable’ storage with a hash/key so FairphoneOS or e/OS would always boot, but it seems not.

SPL: Would be great if we could check that from fastboot or at least fastboot could tell us if Anti-Rollback-Protection is triggered with the currently flashed partitions, but i did not find such a functionality :frowning:

My phone tells me: Security Patch Platform: 1. June 2025
Current FairphoneOS image: FP5-VT2D-factory.zip (Security patch level: 05 August 2025)
Current iodéOS image has no information and they do not post about all of their updates. The news app seems to pull this info (scroll all the way down for the newest entry): posts.json · main · ota / news · GitLab and it states: [..] Changelog: - LineageOS synchronized with August security patch [..] so i guess this is the same SPL and should be fine to flash after FairphoneOS.

This is “flash-all.sh” from the latest iodéOS FP5 image:
There is no mention of A/B so i guess they flash the currently active partition only?

#!/bin/sh

# Copyright 2012 The Android Open Source Project
# Copyright 2023 Iodé Technologies
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

export ANDROID_PRODUCT_OUT=.
fastboot=linux/fastboot
$fastboot erase avb_custom_key
$fastboot flash avb_custom_key avb_custom_key-FP5.bin
$fastboot getvar all 2>&1 | grep "partition-type:cache" > /dev/null && $fastboot format:ext4 cache && $fastboot erase cache
$fastboot getvar all 2>&1 | grep "partition-type:metadata" > /dev/null && $fastboot format:ext4 metadata && $fastboot erase metadata
$fastboot getvar all 2>&1 | grep "partition-type:md_udc" > /dev/null && $fastboot format:ext4 md_udc && $fastboot erase md_udc
$fastboot format:f2fs userdata
$fastboot flash modem modem.img
$fastboot --skip-reboot update iode-6.7-20250905-FP5-img.zip
$fastboot erase userdata
$fastboot getvar is-userspace 2>&1 | grep -q "^is-userspace: yes" || $fastboot reboot fastboot && sleep 5
$fastboot flash abl abl.img
$fastboot flash aop aop.img
$fastboot flash bluetooth bluetooth.img
$fastboot flash cpucp cpucp.img
$fastboot flash devcfg devcfg.img
$fastboot flash dsp dsp.img
$fastboot flash featenabler featenabler.img
$fastboot flash hyp hyp.img
$fastboot flash imagefv imagefv.img
$fastboot flash keymaster keymaster.img
$fastboot flash multiimgoem multiimgoem.img
$fastboot flash qupfw qupfw.img
$fastboot flash shrm shrm.img
$fastboot flash tz tz.img
$fastboot flash uefisecapp uefisecapp.img
$fastboot flash xbl_config xbl_config.img
$fastboot flash xbl xbl.img
$fastboot reboot-bootloader
sleep 5
$fastboot flashing lock

Yes thank you, will do that.
I was using my reinstall as an exmaple and wanted to be extra clear to maybe learn something along the way :wink:

Hi,
better to bundle your answers together to avoid multiposts.
When you select and quote a previous message, it adds up to a message you’ve begun writing already.

I know there’s a variable which encodes this at really low level, but I know of no way to read it via fastboot. It’s in your settings.

Yeah. Seems good! As yvmuell pointed out, it’s probably not necessary, but still I think it’s a good practice, since the flashing ROM script might be assuming that you’re coming from stock.

I’ll definitely download this archive first opportunity cause it’s puzzling. Some partitions are not here. These are modem/firmware partitions but i don’t see system or boot for instance, which are the “true” partitions that encode your OS.
It might be done in recovery or in the “update iode-6.7…zip”.

Can you tell me more about that? Can i read the variable via adb in a running system?

Yes you are right, i missed that.
The flash-all script uses “update” for the .zip file (update ZIP: Flash all partitions from an update.zip package). Inside the .zip are these files:

abl.img
android-info.txt
aop.img
bluetooth.img
boot.img
cpucp.img
devcfg.img
dsp.img
dtbo.img
fastboot-info.txt
featenabler.img
hyp.img
imagefv.img
keymaster.img
modem.img
multiimgoem.img
odm.img
product.img
qupfw.img
shrm.img
super_empty.img
system_ext.img
system.img
tz.img
uefisecapp.img
unsparse_super_empty.img
vbmeta.img
vbmeta_system.img
vendor_boot.img
vendor.img
xbl_config.img
xbl.img

So i have to update my previous message, the “vbmeta*”" partitions are flashed by iodéOS after all.

But it seems they only flash the currently running slot, which makes sense to me as you always have the other slot as fallback.

I don’t know how the update zip command works, but since no slot is mentioned, I would wager it would flash both.
My reasoning is that the two slots work by difference between a given version and the next. Fundamentally, they should be populated with two successive version of the same OS to work as intended.
The safeguard is more about OTA (wireless) updates. The unused slot is written into when you click update and continue using your phone. Then just reboot and you switch slots, creating a seamless experience (to the extent that a reboot is seamless).

Did the installation now as i mentioned earlier.

I flashed FairphoneOS and after flashing the script rebooted and i got the Welcome/Onboarding screen.
Then manually rebooted to the bootloader and get_unlock_ability was ‘0’.
Flashed FairphoneOS again and did not reboot and then it was ‘1’.

Do we know why we have to lock before booting into the OS ?


Also here is the log output when flashing iodéOS, maybe it helps shed some light on the flashing A/B partition thing (looks to me that only partition ‘a’ was flashed).
Also when the script prints “Rebooting into fastboot [..] < waiting for any device >'” the phone rebooted in some sort of bootloader / flasher i have not seen before during other OS flash scripts.

./flash-all.sh

Erasing 'avb_custom_key'                           OKAY [  0.011s]
Finished. Total time: 0.016s
Warning: skip copying avb_custom_key image avb footer (avb_custom_key partition size: 0, avb_custom_key image size: 1032).
Sending 'avb_custom_key' (1 KB)                    OKAY [  0.001s]
Writing 'avb_custom_key'                           OKAY [  0.008s]
Finished. Total time: 0.025s
mke2fs 1.46.6 (1-Feb-2023)
Creating filesystem with 4096 4k blocks and 4096 inodes

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

Warning: skip copying metadata image avb footer due to sparse image.
Sending 'metadata' (52 KB)                         OKAY [  0.002s]
Writing 'metadata'                                 OKAY [  0.004s]
Finished. Total time: 0.025s
******** Did you mean to fastboot format this ext4 partition?
Erasing 'metadata'                                 OKAY [  0.004s]
Finished. Total time: 0.009s

    F2FS-tools: mkfs.f2fs Ver: 1.16.0 (2023-04-11)

Info: Debug level = 1
Info: Trim is disabled
Info: Set conf for android
Info: Enable Project quota
Info: not exist /proc/version!
Info: Segments per section = 1
Info: Sections per zone = 1
Info: sector size = 512
Info: total sectors = 448795544 (219138 MB)
Info: zone aligned segment0 blkaddr: 512
Info: add quota type = 0 => 4
Info: add quota type = 1 => 5
Info: add quota type = 2 => 6
Info: format version with
  "6.16.5-arch1-1"
[f2fs_init_sit_area: 714]       Filling sit area at offset 0x00600000
[f2fs_init_nat_area: 748]       Filling nat area at offset 0x01600000
[f2fs_add_default_dentry_root:1364]     Writing default dentry root, at offset 0x2a800
[f2fs_write_root_inode:1410]    Writing root inode (hot node), offset 0x2a200
[f2fs_write_default_quota:1502]         Writing quota data, at offset 0002a801 (1/2)
[f2fs_write_default_quota:1502]         Writing quota data, at offset 0002a802 (2/2)
[f2fs_write_qf_inode:1553]      Writing quota inode (hot node), offset 0x2a201
[f2fs_write_default_quota:1502]         Writing quota data, at offset 0002a803 (1/2)
[f2fs_write_default_quota:1502]         Writing quota data, at offset 0002a804 (2/2)
[f2fs_write_qf_inode:1553]      Writing quota inode (hot node), offset 0x2a202
[f2fs_write_default_quota:1502]         Writing quota data, at offset 0002a805 (1/2)
[f2fs_write_default_quota:1502]         Writing quota data, at offset 0002a806 (2/2)
[f2fs_write_qf_inode:1553]      Writing quota inode (hot node), offset 0x2a203
[f2fs_update_nat_default:1591]  Writing nat root, at offset 0x00001600
Info: Overprovision ratio = 0.310%
Info: Overprovision segments = 343 (GC reserved = 329)
[f2fs_write_check_point_pack: 939]      Writing main segments, cp at offset 0x00000200
[f2fs_write_check_point_pack:1027]      Writing Segment summary for HOT/WARM/COLD_DATA, at offset 0x00000201
[f2fs_write_check_point_pack:1041]      Writing Segment summary for HOT_NODE, at offset 0x00000202
[f2fs_write_check_point_pack:1053]      Writing Segment summary for WARM_NODE, at offset 0x00000203
[f2fs_write_check_point_pack:1067]      Writing Segment summary for COLD_NODE, at offset 0x00000204
[f2fs_write_check_point_pack:1075]      Writing cp page2, at offset 0x00000205
[f2fs_write_check_point_pack:1095]      Writing NAT bits pages, at offset 0x000003fe
[f2fs_write_check_point_pack:1117]      Writing cp page 1 of checkpoint pack 2, at offset 0x00000400
[f2fs_write_check_point_pack:1136]      Writing cp page 2 of checkpoint pack 2, at offset 0x00000405
[f2fs_write_super_block:1169]   Writing super block, at offset 0x00000000
Info: format successful
Warning: skip copying userdata image avb footer due to sparse image.
Sending 'userdata' (97 KB)                         OKAY [  0.003s]
Writing 'userdata'                                 OKAY [  0.010s]
Finished. Total time: 0.236s
Sending 'modem_a' (186076 KB)                      OKAY [  3.986s]
Writing 'modem_a'                                  OKAY [  0.530s]
Finished. Total time: 6.274s
--------------------------------------------
Bootloader Version...: FP5.VT2D.C.054.20250814
Baseband Version.....: 13
Serial Number........: 6d2feeb4
--------------------------------------------
extracting android-info.txt (0 MB) to RAM...
Checking 'product'                                 OKAY [  0.002s]
Setting current slot to 'a'                        OKAY [  0.002s]
extracting fastboot-info.txt (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to disk... took 0.000s
Writable partitions are not supported
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting super_empty.img (0 MB) to RAM...
extracting boot.img (96 MB) to disk... took 0.253s
archive does not contain 'boot.sig'
extracting super_empty.img (0 MB) to RAM...
Sending 'boot_a' (98304 KB)                        OKAY [  2.102s]
Writing 'boot_a'                                   OKAY [  0.264s]
extracting super_empty.img (0 MB) to RAM...
extracting dtbo.img (24 MB) to disk... took 0.041s
archive does not contain 'dtbo.sig'
extracting super_empty.img (0 MB) to RAM...
Sending 'dtbo' (24576 KB)                          OKAY [  0.515s]
Writing 'dtbo'                                     OKAY [  0.095s]
extracting super_empty.img (0 MB) to RAM...
extracting vendor_boot.img (96 MB) to disk... took 0.170s
archive does not contain 'vendor_boot.sig'
extracting super_empty.img (0 MB) to RAM...
Sending 'vendor_boot' (98304 KB)                   OKAY [  2.078s]
Writing 'vendor_boot'                              OKAY [  0.284s]
extracting super_empty.img (0 MB) to RAM...
extracting vbmeta.img (0 MB) to disk... took 0.000s
archive does not contain 'vbmeta.sig'
extracting super_empty.img (0 MB) to RAM...
Sending 'vbmeta' (8 KB)                            OKAY [  0.001s]
Writing 'vbmeta'                                   OKAY [  0.003s]
extracting super_empty.img (0 MB) to RAM...
extracting vbmeta_system.img (0 MB) to disk... took 0.000s
archive does not contain 'vbmeta_system.sig'
extracting super_empty.img (0 MB) to RAM...
Sending 'vbmeta_system' (4 KB)                     OKAY [  0.001s]
Writing 'vbmeta_system'                            OKAY [  0.003s]
Rebooting into fastboot                            OKAY [  0.002s]
< waiting for any device >
extracting super_empty.img (0 MB) to disk... took 0.000s
Sending 'super' (4 KB)                             OKAY [  0.001s]
Updating super partition                           OKAY [  0.007s]
Resizing 'odm_a'                                   OKAY [  0.004s]
Resizing 'product_a'                               OKAY [  0.004s]
Resizing 'system_a'                                OKAY [  0.004s]
Resizing 'system_ext_a'                            OKAY [  0.004s]
Resizing 'vendor_a'                                OKAY [  0.004s]
extracting super_empty.img (0 MB) to RAM...
extracting odm.img (1 MB) to disk... took 0.018s
archive does not contain 'odm.sig'
Resizing 'odm_a'                                   OKAY [  0.004s]
Sending 'odm_a' (1452 KB)                          OKAY [  0.032s]
Writing 'odm_a'                                    OKAY [  0.082s]
extracting super_empty.img (0 MB) to RAM...
extracting product.img (1041 MB) to disk... took 4.131s
archive does not contain 'product.sig'
Resizing 'product_a'                               OKAY [  0.004s]
Sending sparse 'product_a' 1/5 (261948 KB)         OKAY [  5.870s]
Writing 'product_a'                                OKAY [  0.614s]
Sending sparse 'product_a' 2/5 (262056 KB)         OKAY [  5.989s]
Writing 'product_a'                                OKAY [  0.506s]
Sending sparse 'product_a' 3/5 (262040 KB)         OKAY [  5.923s]
Writing 'product_a'                                OKAY [  0.584s]
Sending sparse 'product_a' 4/5 (259581 KB)         OKAY [  5.824s]
Writing 'product_a'                                OKAY [  0.568s]
Sending sparse 'product_a' 5/5 (16648 KB)          OKAY [  0.364s]
Writing 'product_a'                                OKAY [  0.156s]
extracting super_empty.img (0 MB) to RAM...
extracting system.img (944 MB) to disk... took 3.531s
archive does not contain 'system.sig'
Resizing 'system_a'                                OKAY [  0.005s]
Sending sparse 'system_a' 1/4 (262096 KB)          OKAY [  5.893s]
Writing 'system_a'                                 OKAY [  0.576s]
Sending sparse 'system_a' 2/4 (262056 KB)          OKAY [  5.840s]
Writing 'system_a'                                 OKAY [  0.580s]
Sending sparse 'system_a' 3/4 (261972 KB)          OKAY [  5.871s]
Writing 'system_a'                                 OKAY [  0.529s]
Sending sparse 'system_a' 4/4 (176928 KB)          OKAY [  3.951s]
Writing 'system_a'                                 OKAY [  0.413s]
extracting super_empty.img (0 MB) to RAM...
extracting system_ext.img (480 MB) to disk... took 1.818s
archive does not contain 'system_ext.sig'
Resizing 'system_ext_a'                            OKAY [  0.004s]
Sending sparse 'system_ext_a' 1/2 (260466 KB)      OKAY [  5.759s]
Writing 'system_ext_a'                             OKAY [  0.867s]
Sending sparse 'system_ext_a' 2/2 (227944 KB)      OKAY [  5.095s]
Writing 'system_ext_a'                             OKAY [  0.560s]
extracting super_empty.img (0 MB) to RAM...
extracting vendor.img (900 MB) to disk... took 3.141s
archive does not contain 'vendor.sig'
Resizing 'vendor_a'                                OKAY [  0.004s]
Sending sparse 'vendor_a' 1/4 (262032 KB)          OKAY [  5.827s]
Writing 'vendor_a'                                 OKAY [  0.583s]
Sending sparse 'vendor_a' 2/4 (262116 KB)          OKAY [  5.825s]
Writing 'vendor_a'                                 OKAY [  0.515s]
Sending sparse 'vendor_a' 3/4 (262140 KB)          OKAY [  6.010s]
Writing 'vendor_a'                                 OKAY [  0.577s]
Sending sparse 'vendor_a' 4/4 (132344 KB)          OKAY [  3.005s]
Writing 'vendor_a'                                 OKAY [  0.390s]
Finished. Total time: 127.737s
Erasing 'userdata'                                 OKAY [  0.060s]
Finished. Total time: 0.064s
Sending 'abl_a' (152 KB)                           OKAY [  0.004s]
Writing 'abl_a'                                    OKAY [  0.005s]
Finished. Total time: 0.035s
Sending 'aop_a' (203 KB)                           OKAY [  0.005s]
Writing 'aop_a'                                    OKAY [  0.005s]
Finished. Total time: 0.018s
Sending 'bluetooth_a' (3436 KB)                    OKAY [  0.077s]
Writing 'bluetooth_a'                              OKAY [  0.021s]
Finished. Total time: 0.108s
Sending 'cpucp_a' (182 KB)                         OKAY [  0.004s]
Writing 'cpucp_a'                                  OKAY [  0.006s]
Finished. Total time: 0.020s
Sending 'devcfg_a' (52 KB)                         OKAY [  0.002s]
Writing 'devcfg_a'                                 OKAY [  0.003s]
Finished. Total time: 0.012s
Sending 'dsp_a' (65536 KB)                         OKAY [  1.436s]
Writing 'dsp_a'                                    OKAY [  0.154s]
Finished. Total time: 1.596s
Sending 'featenabler_a' (84 KB)                    OKAY [  0.003s]
Writing 'featenabler_a'                            OKAY [  0.005s]
Finished. Total time: 0.017s
Sending 'hyp_a' (3613 KB)                          OKAY [  0.081s]
Writing 'hyp_a'                                    OKAY [  0.024s]
Finished. Total time: 0.117s
Sending 'imagefv_a' (64 KB)                        OKAY [  0.002s]
Writing 'imagefv_a'                                OKAY [  0.003s]
Finished. Total time: 0.014s
Sending 'keymaster_a' (265 KB)                     OKAY [  0.007s]
Writing 'keymaster_a'                              OKAY [  0.006s]
Finished. Total time: 0.020s
Sending 'multiimgoem_a' (13 KB)                    OKAY [  0.001s]
Writing 'multiimgoem_a'                            OKAY [  0.003s]
Finished. Total time: 0.011s
Sending 'qupfw_a' (55 KB)                          OKAY [  0.002s]
Writing 'qupfw_a'                                  OKAY [  0.005s]
Finished. Total time: 0.014s
Sending 'shrm_a' (44 KB)                           OKAY [  0.001s]
Writing 'shrm_a'                                   OKAY [  0.003s]
Finished. Total time: 0.011s
Sending 'tz_a' (3689 KB)                           OKAY [  0.085s]
Writing 'tz_a'                                     OKAY [  0.024s]
Finished. Total time: 0.121s
Sending 'uefisecapp_a' (122 KB)                    OKAY [  0.003s]
Writing 'uefisecapp_a'                             OKAY [  0.005s]
Finished. Total time: 0.017s
Sending 'xbl_config_a' (220 KB)                    OKAY [  0.005s]
Writing 'xbl_config_a'                             OKAY [  0.009s]
Finished. Total time: 0.021s
Sending 'xbl_a' (3602 KB)                          OKAY [  0.083s]
Writing 'xbl_a'                                    OKAY [  0.032s]
Finished. Total time: 0.126s
Rebooting into bootloader                          OKAY [  0.000s]
Finished. Total time: 0.251s

Yes

Its german but we have this info most likely somewhere as well in EN

Yes this phenomenon is known. However we have but guesses as of why it really is. The stock installation of an official OS might warrant some security standards, and one of them would be to de-activate OEM-unlocking, with the understanding that the bootloader is locked (coming from the factory), so that the end-user, when they need to, might go and activate OEM-unlocking by the online form with the code.
So, when you boot “to userspace” with an unlocked bootloader, OEM-unlocking is still deactivated, and leaves you with an unlocked bootloader and no way to go and input that code to get OEM-unlocking to work.

Only solution, as you did, was to reinstall stock and lock before booting up the first time.

And so it seems. My guess is the b_slot will be populated next OTA update. But I still don’t know if you can really “go back” to the OS which would inhabit slot_b, though. Maybe the A/B slot system is unusable for now…?

I’m sorry but it doesn’t.
I was under the impression of the following logic, assuming the best conditions (stock OS, latest SPL) :
Flash and first boot (no lock) → OEM unlocking disabled —> get_flashing_ability=0 —> brick if lock.

If feel that you’re trying to say that it won’t brick if and only if the SPL is the latest?

It should not brick when the SPL is newer or the same. However there are some FP6 user stating they did not flash an older SPL and still the phone was a brick after locking.

Still “cant” (not possible) and shouldnt (not advisable) is not the same.

The brick isnt happening because of the get unlock ability or its only 1 part of the story and the real cause normally is the SPL and maybe (we dont know) on the FP6 a bug in the bootloader

get_unlock_ability=0 just means you cannot unlock if something wrong, like the rollback protection.

So to clarify:

  • if get_unlock_ability=1
    • Locking with newer patch version = OK
    • Locking with older patch version = not OK, but you can unlock :white_check_mark:
  • if get_unlock_ability=0
    • Locking with newer patch version = OK
    • Locking with older patch version = brick :brick:

That’s why we (the forum users, not fairphone!) always advice to not lock if ability=0 :slight_smile:

Thanks everyone for your answers.

As yvmuell explained, the phones did brick when the SPL was the same at a time when the only two ROMs for the FP6 were dated June 2025. So, is there more to it or is it specific to the FP6?

Do you have any testimony of someone locking and then unlocking despite having a get_unlock_ability of 0?
Or maybe some technical documents with an accurate flowchart?

Kindly read above and in the FP4 topic Volker linked..for the FP4 (which has a bootloader bug), we have all kinds of variables documented in the forum

So there’s an unspecified bug for the FP4 and the FP6 that does the same consistent thing across all devices?
Let’s say the FP5 has that specific command that allows flashing.
So, for the FP3 for instance, there is no risk of bricking if you respect the rollback protection? The FP2 too?

The FP2 has no locked bootloader.

And no-one can exclude risks, still FP3 and FP5 seem to work as expected in this regard while the FP4 has a bootloader bug (not confirmed by FP) and for the FP6 as documented here the FP6 topic.

Thats completely going off topic btw.

Not completely. However, I suggest we take this to the wiki-discussion about the bricking guide. I wrote the article with the understanding that I laid out earlier.

I’m still not sure that what you call a bug is not normal behavior. As I understand it, the unlock ability can control a lock ability with the same variable. It would make sense to me that if you can’t unlock, then you can’t lock either. In a way, it would make partitions read-only or read-write

No, what I call a bug is a bug and its very well documented in the FP4 topics. Also the info in the FP6 topic suggests there is a bug, however its unclear where.

Impression is wrong.
No reliable testimony for this assumption.
But a testimony of someone having locked an FP6 bootloader with a phone with get_flashing_ability=0 and no problem:

Sorry, the topic is in German. But I’m sure you’ll be able to translate it if necessary.

So the flow description of @oli.sax is perfectly describing it.

EDIT, addition:

Sorry, also not correct.
There’s no “variable” for lock ability. With “OEM unlock” disabled the fastboot command to unlock just creates an error message.
There’s no error message when using the lock command in fastboot. This always works. The interesting question is then only if the device boots or not (based on SPL history).

I mean, dozens of testimonies like “I had unlock_ability = 0 and I bricked it” and months of advice like “if you have unlock_ability =0 don’t lock” sure helped to forge this conclusion. It’s so systematic and widespread it’s logical to see this as a feature and not a bug, owing to Occam’s Razor. I have yet to see any official statement or technical document to the contrary.

As for the single case where unlock_ability=0 turned out fine, I had little chance to analyze it.
Did you do it yourself?
As I understand it, you waited for an OTA before locking.
What versions are we talking about? stock or /e/OS or another?
Did the OTA un-grayed out “OEM Unlocking”? Did you activate OEM Unlocking first?
Just before locking, did you confirm that unlock ability was still =0?
What if you had unlocked critical? Do you think it would have changed things?