I had a Samsung Galaxy A71 5G lying around.

It’s not particularly new. It’s not particularly valuable. And, before I factory-reset it, Samsung had been periodically warning me that its battery was deteriorating.

After the factory reset, however, the warning disappeared.

Which made me curious.

Had the battery magically healed itself?

Obviously not.

So I started poking around.

My original plan was extremely reasonable:

Check the actual battery health and see how much life the internal flash storage has left.

A few hours later I had:

  • unlocked the bootloader;
  • downloaded Samsung’s original firmware;
  • patched it with Magisk;
  • discovered that Odin4 wouldn’t talk properly to this phone from Linux;
  • switched to Thor;
  • found two bugs in Thor;
  • patched and rebuilt Thor;
  • started flashing the phone;
  • watched it die halfway through a 6.8 GiB partition;
  • discovered a Linux USB memory limitation;
  • flashed it again;
  • recovered Android;
  • obtained root;
  • interrogated the UFS controller directly;
  • pulled Samsung’s internal battery-health data;
  • and disabled close to a hundred pieces of Samsung, Google, Microsoft, Meta and carrier cruft.

So, you know.

A quick battery check.


The patient

The phone was an international Galaxy A71 5G:

Samsung Galaxy A71 5G
Model:    SM-A716B
Android:  13
Firmware: A716BXXS9FXA1
CSC:      XSG
Storage:  128 GB UFS

My computer runs Xubuntu 24.04.

The phone had already been factory-reset, which made it an unusually good victim for experimentation. There was no data I cared about losing.

That detail becomes rather important later.


It started with dumpsys battery

ADB gives you a decent amount of battery information without root:

adb shell dumpsys battery

Samsung reported the battery as healthy.

More interestingly, buried in Samsung’s additional battery data was:

mSavedBatteryAsoc = 90

That immediately caught my attention.

ASOC is Samsung’s battery state-of-health estimate.

The phone that had previously been nagging me about battery deterioration appeared to believe its battery was still at approximately 90% health.

I wanted more.

Specifically, I wanted access to the actual battery fuel-gauge telemetry and the UFS health descriptors.

Android politely responded to those attempts with some variation of:

Permission denied

Fair enough.

Root it is.


Unlocking the bootloader

Samsung’s OEM Unlock option was available.

Unlocking a Samsung bootloader triggers a factory reset, but the phone was already disposable from a data perspective, so I went ahead.

After the unlock and another setup cycle, Developer Options showed:

OEM unlocking: Enabled

and the toggle was greyed out.

Good.

The bootloader was properly unlocked.

There is one important caveat here: unlocking and modifying Samsung devices can trip Knox and has consequences for Samsung security features. Anyone following along should understand that before doing this to a phone they actually depend on.

This isn’t a “click these commands and everything will definitely be fine” guide.

It’s a record of what happened on my particular SM-A716B.


Getting firmware without visiting a sketchy firmware mirror

For Magisk root on this Samsung, I wanted the exact stock firmware currently installed on the phone.

I used Bifrost to retrieve the firmware directly from Samsung’s servers.

The build was:

A716BXXS9FXA1

The firmware ZIP contained the familiar Samsung collection:

BL_A716B....tar.md5
AP_A716B....tar.md5
CP_A716B....tar.md5
CSC_OJM_A716B....tar.md5
HOME_CSC_OJM_A716B....tar.md5

Very roughly:

BL   bootloader / low-level firmware
AP   Android, boot, recovery, system partitions
CP   modem/baseband
CSC  regional/carrier configuration

The AP archive was enormous because it contained things like:

boot.img.lz4
recovery.img.lz4
dtbo.img.lz4
super.img.lz4
metadata.img.lz4
vbmeta.img.lz4
...

super.img is where modern Android keeps the dynamic system partitions, and on this firmware it was going to become the villain of the story.

But we hadn’t met yet.


Magisk patches AP

I installed Magisk 30.7 and gave it the complete Samsung AP archive.

Magisk generated:

magisk_patched-30700_....tar

One nice detail became important later.

The original Samsung archive contained:

boot.img.lz4
vbmeta.img.lz4

while Magisk’s patched archive contained:

boot.img
vbmeta.img

alongside other images that remained .lz4 compressed.

That is perfectly valid.

It would also expose a bug in the flashing tool we were about to use.


Surely flashing Samsung firmware from Linux is solved by now

Samsung’s official flashing tool is Odin.

Samsung does not officially provide Odin for Linux, but there are open-source implementations.

I started with Odin4.

It built after a couple of modern Linux annoyances involving Qt and the compiler toolchain, and eventually I had:

odin4 version 7.3.0-a46321b

The phone appeared normally over USB in Download Mode:

04e8:685d Samsung Electronics

Odin4 found the right USB interface and endpoints.

Then came the encouraging bit:

Starting handshake
Handshake successful.

Excellent.

Followed immediately by the less encouraging bit:

USB bulk read failed
LIBUSB_ERROR_TIMEOUT (-7)

Failed to begin session.

I stopped ModemManager.

I checked USB interfaces.

I experimented with the cdc_acm driver.

I re-entered Download Mode.

Same result.

The USB connection was clearly working — Odin4 could see the phone and complete its initial handshake — but it couldn’t establish the actual Odin flashing session.

At some point you have to stop blaming the cable.


Enter Thor

The next candidate was Thor, another open-source implementation of Samsung’s Odin protocol.

Thor is written in .NET.

I launched it:

sudo ./Thor-Linux

and got:

Welcome to Thor Shell v1.1.0!

Then:

connect

Response:

Successfully connected to the device!

And finally:

begin odin

Response:

Successfully began an Odin session!

That was interesting.

Thor had just succeeded at precisely the point Odin4 repeatedly failed.

So suddenly a lot of possible causes disappeared:

Phone USB hardware      ✓
Cable                   ✓
Linux USB stack         ✓
Samsung Download Mode   ✓
Odin protocol possible  ✓

Odin4 on this device    ¯\_(ツ)_/¯

I thought we were ready to flash.

Thor had other ideas.


Bug number one: apparently my storage technology is too modern

Samsung devices contain a PIT, or Partition Information Table.

Thor can retrieve it:

printPit

So I did.

Thor crashed.

System.IndexOutOfRangeException
Index was outside the bounds of the array.

Excellent.

Fortunately:

dumpPit /home/omar/a71.pit

worked.

The resulting PIT was valid and Heimdall could parse it.

One entry revealed an important clue:

Device Type: 8

The A71 uses UFS storage.

I went into Thor’s source.

The PrintPIT code contained lookups roughly like this:

mapper.DeviceType[entry.DeviceType + 1]

The mapping array simply didn’t contain every value a modern Samsung PIT might return.

So rather than handling an unknown type gracefully, Thor walked off the end of its array.

The project already had the beginnings of a safe lookup helper.

I changed it to:

public static string GetMapping(this string[] array, int index)
    => index < 0 || index >= array.Length
        ? "Unknown"
        : array[index];

and used that for the PIT mappings.

Rebuild.

Run Thor.

printPit

And this time:

it worked.

That was bug number one.


Bug number two: .lz4

Now for the actual firmware.

Thor’s:

flashTar

command is supposed to inspect Samsung’s TAR files, match each contained image with the corresponding PIT partition, and present a list of partitions to flash.

Instead I got:

Cannot show an empty selection prompt.
Please call the AddChoice() method to configure the prompt.

An empty list.

Back to the source code.

Thor was doing essentially this:

x.FileName == entry.Name

That looks reasonable until you compare the files.

Samsung’s firmware archive:

boot.img.lz4
recovery.img.lz4
super.img.lz4

Samsung’s PIT:

boot.img
recovery.img
super.img

So:

super.img.lz4 != super.img

Thor consequently concluded there was nothing to flash.

The fix was tiny:

var flashName = entry.Name.EndsWith(
        ".lz4",
        StringComparison.OrdinalIgnoreCase)
    ? entry.Name[..^4]
    : entry.Name;

var pitEntry = data.Entries.FirstOrDefault(
    x => string.Equals(
        x.FileName,
        flashName,
        StringComparison.OrdinalIgnoreCase));

Rebuild again.

Run again.

And suddenly Thor understood the firmware.

It correctly mapped things like:

recovery.img.lz4       -> RECOVERY
dtbo.img.lz4           -> DTBO
super.img.lz4          -> SUPER
metadata.img.lz4       -> METADATA
vbmeta.img             -> VBMETA
vbmeta_samsung.img.lz4 -> VBMETA_SAMSUNG
misc.bin.lz4           -> MISC
boot.img               -> BOOT

Most importantly:

Magisk boot.img -> BOOT

We now had an open-source Samsung flashing tool, running on Linux, correctly understanding the firmware.

So naturally this was when I decided to press the button.


22 partitions and one very large problem

The final flash consisted of the original:

BL
CP
CSC

and the Magisk-patched:

AP

Thor selected 22 partitions in total.

The flashing process began beautifully.

Partition after partition completed.

Then it reached:

SUPER

SUPER was approximately 6.8 GiB and Thor divided it into 233 transfer sequences.

I watched:

1 / 233
10 / 233
30 / 233
50 / 233
...
64 / 233

And then:

Failed to bulk write:
Cannot allocate memory (12)

Thor died.

Approximately 1.9 GiB of a 6.8 GiB Android system partition had been written.

That is not an ideal place to stop.

At this point Android was unquestionably broken.


“Have I bricked it?”

The phone eventually displayed Samsung’s cheerful:

An error has occurred while updating the device software.

But this distinction matters:

Download Mode still worked.

The low-level bootloader was alive.

The phone wasn’t hard-bricked. Its Android installation was simply mangled.

A soft brick is mostly a scary-looking invitation to flash it again.

Still, seeing Samsung’s giant software-error screen immediately after partially overwriting SUPER does concentrate the mind wonderfully.


The laptop had plenty of RAM

The error said:

Cannot allocate memory

My laptop had roughly:

19 GiB RAM
~12 GiB available

So this clearly wasn’t ordinary process memory exhaustion.

The errno was coming from USB:

ENOMEM (12)

That led me to Linux’s usbfs configuration:

cat /sys/module/usbcore/parameters/usbfs_memory_mb

Output:

16

16 MB.

Linux limits how much memory usbfs may allocate for USB transfer buffers.

Thor was performing large sustained USB transfers while flashing a 6.8 GiB image.

So I raised the limit:

echo 256 | sudo tee \
    /sys/module/usbcore/parameters/usbfs_memory_mb

Verification:

cat /sys/module/usbcore/parameters/usbfs_memory_mb
256

I briefly considered modifying Thor’s transfer packet size too.

Then decided against it.

One variable at a time.

That decision turned out to be useful, because it let me know exactly what fixed the problem.


Here we go again

I forced the phone back into Download Mode.

Started Thor.

Connected.

Started another Odin session.

Selected the entire firmware again.

And flashed it from the beginning.

Eventually:

SUPER

appeared.

The previous failure point approached.

50 / 233
60 / 233
64 / 233

Then:

65
66
67
...

Beautiful.

100
150
200
...
233 / 233

All 6.8 GiB transferred.

Thor then completed:

METADATA
VBMETA
VBMETA_SAMSUNG
MISC
BOOT
RADIO
CP_DEBUG

Everything reached 100%.

The second flash had worked.

The only change:

usbfs_memory_mb: 16 -> 256

That was probably my favourite discovery of the entire exercise.


Of course, Thor had one last surprise

After the successful flash I ran:

end

Thor timed out.

Failed to bulk read:
Connection timed out (110)

At this stage I’d spent enough time inside Thor’s source code that looking at it again seemed perfectly normal.

The generic end path attempted a shutdown operation.

Another command existed:

reboot normal

and its implementation performed:

odin.EndSession();
odin.Reboot();

That looked much closer to what I wanted.

I established a clean Odin session again and ran:

reboot normal

The phone rebooted.

Lesson number three:

On this phone, with this Thor version, reboot normal was the clean way out.


Android Recovery

Samsung Recovery appeared.

/data needed to be recreated, so I performed:

Factory data reset
Reboot system now

Then came the long Samsung boot logo.

And finally:

Android.

Alive.

At this point I had transformed a perfectly functional phone into a broken phone, repaired the software I was using to repair it, broken the phone harder, repaired Linux, repaired the phone again, and ended up approximately where I started.

Except now I had root.

Progress.


uid=0(root)

Magisk completed its additional setup and rebooted once.

Then:

adb shell
su
id

returned:

uid=0(root)

Finally.

And remember why we did all of this?

I wanted to know whether the storage and battery were healthy.

So now I could simply ask them.


Asking the UFS controller how close it is to death

The A71’s internal flash identifies as:

SAMSUNG
KM8V8001JM-B813
Revision 0003

The kernel exposes UFS health descriptors directly.

On this device:

cat /sys/devices/platform/13600000.ufs/health_descriptor/life_time_estimation_a
cat /sys/devices/platform/13600000.ufs/health_descriptor/life_time_estimation_b
cat /sys/devices/platform/13600000.ufs/health_descriptor/eol_info

returned:

0x04
0x04
0x01

The lifetime estimates are intentionally coarse.

0x04 corresponds to roughly 30–40% of the rated endurance having been consumed.

The pre-EOL value:

0x01

means:

NORMAL

So the storage was nowhere near the end of its rated life.

Other UFS status values were equally boring:

exception_event_status = 0
bkops_status           = 0
ffu_status             = 0
purge_status           = 0

Kernel logs contained no suspicious UFS resets, timeouts or I/O failures.

Excellent.


Let’s write a gigabyte to it anyway

For good measure:

cd /data/local/tmp

dd if=/dev/zero \
   of=ufs_test.bin \
   bs=4M count=256 \
   conv=fsync

Result:

~354 MB/s write

Then:

dd if=ufs_test.bin \
   of=/dev/null \
   bs=4M

gave around:

~718 MB/s read

The read result can be influenced by caching, so I wouldn’t publish it as a storage benchmark.

The more meaningful result was:

1 GiB written
1 GiB read
0 I/O errors

The UFS looked healthy.


And the supposedly dying battery?

Now root exposed Samsung’s internal battery information.

Two different places agreed:

mSavedBatteryAsoc = 90
fg_asoc           = 90

Samsung’s CISD telemetry also said:

ASOC = 90

So the fuel gauge itself considered the battery to be around 90% state of health.

Other interesting historical values included:

FULL_CNT:          510
HIGH_SWELLING_CNT: 15
BATT_THM_MAX:      505
CHG_THM_MAX:       566

Those temperature values correspond approximately to:

Battery maximum:         50.5°C
Charging circuitry max:  56.6°C

The phone had lived a life.

But it wasn’t dying.

Current Android health status was:

Good

So after everything we’d just done, the answer to my original question was:

Yes. The battery and storage are actually fine.

I could probably have stopped here.

I did not stop here.


Now that I have root, what’s all this rubbish?

A fresh Samsung installation contains a frankly impressive amount of software that I have absolutely no intention of using.

Bixby.

Samsung Free.

AR Emoji.

AR Zone.

Samsung Pay.

Samsung Pass.

Samsung Cloud.

Game Launcher.

Game Optimizing Service.

Facebook services.

Netflix activation.

Microsoft integration.

OneDrive.

Google Photos.

Chrome.

Google TV.

Carrier recommendation services.

And innumerable packages whose purposes range from “legitimate platform component” to “why does this exist?”

Rather than deleting system APKs, I used Android’s package manager:

pm disable-user --user 0 PACKAGE

This is important.

Disabled is reversible.

For example:

pm enable --user 0 PACKAGE

brings it back.

I worked in batches and rebooted between them.

After each round I tested:

Wi-Fi
Bluetooth
camera
fingerprint
Settings
Play Store
telephony-related components

Eventually I’d disabled close to a hundred packages while leaving Android’s actual core functionality intact.


Samsung Dressroom: a cautionary tale

One package was called:

com.samsung.android.app.dressroom

“Dressroom.”

Obviously essential operating-system infrastructure.

I disabled it.

A little later I noticed:

Settings
  -> Wallpaper and style

no longer worked.

The button was there.

It received the tap animation.

And then:

Nothing.

So I started restoring the obvious suspects:

com.samsung.android.dynamiclock
com.samsung.android.themecenter
com.samsung.android.themestore

Nope.

Wallpaper settings were still dead.

Eventually:

pm enable --user 0 \
    com.samsung.android.app.dressroom

And instantly:

Wallpaper & Style came back.

Of course it did.

We subsequently disabled:

dynamiclock
themecenter
themestore

again.

Wallpaper continued working.

The essential component was:

com.samsung.android.app.dressroom

There is probably a Samsung engineer somewhere who can explain this package name.

I don’t want to know.

Rule of Android debloating: package names are hints, not documentation.


Samsung had one final suggestion

After several rounds of debloating, a notification appeared:

Get recommended apps

Tap here to install apps from your service provider.

No.

Using:

dumpsys notification --noredact

revealed the responsible application:

com.samsung.android.app.omcagent

So:

pm disable-user --user 0 \
    com.samsung.android.app.omcagent

The notification disappeared immediately.

The phone and I had reached an understanding.


Making the debloat reproducible

I eventually consolidated everything into:

/data/local/tmp/debloat-packages.txt
/data/local/tmp/debloat.sh
/data/local/tmp/restore.sh

The package list is the canonical definition of what I don’t want.

The basic debloat script is deliberately boring:

#!/system/bin/sh

LIST="/data/local/tmp/debloat-packages.txt"

while IFS= read -r pkg; do
    [ -z "$pkg" ] && continue

    if pm path "$pkg" >/dev/null 2>&1; then
        echo "DISABLE: $pkg"
        pm disable-user --user 0 "$pkg"
    fi
done < "$LIST"

restore.sh lets me selectively turn packages back on if I discover another “Dressroom.”

I keep copies on the laptop too.

That means after a future firmware update I can reproduce the configuration rather than spending another evening rediscovering every Samsung package.


No Google account required

I also wanted to see how pleasant Android could be without signing the phone into Google.

Quite pleasant, as it turns out.

I installed applications directly from their official distributions, including:

WhatsApp
Telegram X
DuckDuckGo
Termux
FUTO Keyboard

ADB installation is simply:

adb install application.apk

and updates can generally be installed with:

adb install -r application.apk

as long as the package is signed by the same developer key.

Termux became particularly useful because, with Magisk installed:

su

gives me a root shell directly on the phone.

No laptop required.


What actually fixed what

If I had to repeat this entire process tomorrow, these are the details I would care about.

Thor could establish an Odin session when Odin4 couldn’t

On this particular:

SM-A716B
A716BXXS9FXA1

Odin4 completed its initial USB handshake but repeatedly timed out beginning the flashing session.

Thor worked.

That doesn’t mean Odin4 is universally broken or Thor universally superior. It means this specific combination worked for me.

Thor 1.1.0 needed two source fixes

First, PIT output needed bounds-safe mapping for newer values such as the A71’s UFS device type.

Second, flashTar needed to normalize:

foo.img.lz4

to:

foo.img

before matching TAR members against PIT filenames.

Linux usbfs memory mattered

This:

cat /sys/module/usbcore/parameters/usbfs_memory_mb

initially returned:

16

and a 6.8 GiB SUPER flash repeatedly died with USB ENOMEM.

After:

echo 256 | sudo tee \
    /sys/module/usbcore/parameters/usbfs_memory_mb

the complete transfer succeeded.

Don’t panic just because Android is dead

If Samsung Download Mode remains accessible, a failed Android flash is not necessarily a brick.

Don’t delete system APKs while debloating

Use:

pm disable-user --user 0 PACKAGE

first.

com.samsung.android.app.dressroom is Exhibit A.


The final machine

What started as an old Galaxy A71 that Samsung had once complained was deteriorating ended up as:

Samsung Galaxy A71 5G
Android 13
Bootloader unlocked
Magisk 30.7
Root access

Storage:

Samsung KM8V8001JM-B813 UFS
30–40% rated endurance consumed
Pre-EOL: Normal
No observed UFS errors
~354 MB/s sequential test write

Battery:

Samsung ASOC: ~90%
Android health: Good

Software:

Aggressively debloated
No Samsung account
No Google account required
Termux root shell
Reproducible debloat scripts

The phone is now cleaner, more inspectable and arguably more useful to me than it was when Samsung shipped it.


The absurd summary

I wanted to answer:

Is the battery in this phone still okay?

The procedure I eventually used was:

  1. Unlock the bootloader.
  2. Factory-reset the phone.
  3. Download 8 GB of Samsung firmware.
  4. Patch Android with Magisk.
  5. Build an open-source Odin client.
  6. Discover that it doesn’t work with the phone.
  7. Switch to another Odin client.
  8. Discover two bugs in that one.
  9. Patch its source code.
  10. Recompile it.
  11. Start flashing the phone.
  12. Partially overwrite Android.
  13. Crash the flashing tool.
  14. Soft-brick the phone.
  15. Debug Linux’s USB subsystem.
  16. Increase the kernel’s usbfs memory allocation.
  17. Flash the entire phone again.
  18. Find another Odin-session bug.
  19. Read more source code.
  20. Recover Android.
  21. Factory-reset it again.
  22. Install Magisk.
  23. Obtain root.
  24. Read the battery health value.

Answer:

90%

Completely sensible use of an evening.

And this is why I like Linux.