From 21c3e0c46961be1ae6d57593a96bc950fa2e5d23 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Thu, 28 Aug 2025 10:32:04 -0300 Subject: [PATCH] docs(blog): add post about the odd CPU core count bug (#1058) * docs(blog): add post about the odd CPU core count bug Signed-off-by: Xe Iaso * chore: spelling Signed-off-by: Xe Iaso --------- Signed-off-by: Xe Iaso --- .github/actions/spelling/expect.txt | 7 + .../ProofOfWorkDiagram/index.jsx | 214 ++++++++++++ .../ProofOfWorkDiagram/styles.module.css | 317 ++++++++++++++++++ docs/blog/2025-08-28-cpu-core-odd/index.mdx | 129 +++++++ .../parc-dsilence.webp | Bin 0 -> 18908 bytes 5 files changed, 667 insertions(+) create mode 100644 docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/index.jsx create mode 100644 docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/styles.module.css create mode 100644 docs/blog/2025-08-28-cpu-core-odd/index.mdx create mode 100644 docs/blog/2025-08-28-cpu-core-odd/parc-dsilence.webp diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 2ef91ce..3a87eee 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -18,6 +18,7 @@ badregexes bbolt bdba berr +bezier bingbot Bitcoin bitrate @@ -82,6 +83,7 @@ dracula dronebl droneblresponse dropin +dsilence duckduckbot eerror ellenjoe @@ -115,6 +117,7 @@ geoip geoipchecker gha GHSA +Ghz gipc gitea godotenv @@ -128,6 +131,7 @@ goyaml GPG GPT gptbot +Graphene grpcprom grw Hashcash @@ -137,6 +141,7 @@ healthcheck healthz hec hmc +homelab hostable htmlc htmx @@ -258,6 +263,7 @@ runlevels RUnlock runtimedir runtimedirectory +Ryzen sas sasl searchbot @@ -333,6 +339,7 @@ wordpress Workaround workdir wpbot +XCircle Xeact xeiaso xeserv diff --git a/docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/index.jsx b/docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/index.jsx new file mode 100644 index 0000000..b4ac477 --- /dev/null +++ b/docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/index.jsx @@ -0,0 +1,214 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import styles from './styles.module.css'; + +// A helper function to perform SHA-256 hashing. +// It takes a string, encodes it, hashes it, and returns a hex string. +async function sha256(message) { + try { + const msgBuffer = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + return hashHex; + } catch (error) { + console.error("Hashing failed:", error); + return "Error hashing data"; + } +} + +// Generates a random hex string of a given byte length +const generateRandomHex = (bytes = 16) => { + const buffer = new Uint8Array(bytes); + crypto.getRandomValues(buffer); + return Array.from(buffer) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); +}; + + +// Icon components for better visual feedback +const CheckIcon = () => ( + + + +); + +const XCircleIcon = () => ( + + + +); + +// Main Application Component +export default function App() { + // State for the challenge, initialized with a random 16-byte hex string. + const [challenge, setChallenge] = useState(() => generateRandomHex(16)); + // State for the nonce, which is the variable we can change + const [nonce, setNonce] = useState(0); + // State to store the resulting hash + const [hash, setHash] = useState(''); + // A flag to indicate if the current hash is the "winning" one + const [isMining, setIsMining] = useState(false); + const [isFound, setIsFound] = useState(false); + + // The mining difficulty, i.e., the required number of leading zeros + const difficulty = "00"; + + // Memoize the combined data to avoid recalculating on every render + const combinedData = useMemo(() => `${challenge}${nonce}`, [challenge, nonce]); + + // This effect hook recalculates the hash whenever the combinedData changes. + useEffect(() => { + let isMounted = true; + const calculateHash = async () => { + const calculatedHash = await sha256(combinedData); + if (isMounted) { + setHash(calculatedHash); + setIsFound(calculatedHash.startsWith(difficulty)); + } + }; + calculateHash(); + return () => { isMounted = false; }; + }, [combinedData, difficulty]); + + // This effect handles the automatic mining process + useEffect(() => { + if (!isMining) return; + + let miningNonce = nonce; + let continueMining = true; + + const mine = async () => { + while (continueMining) { + const currentData = `${challenge}${miningNonce}`; + const currentHash = await sha256(currentData); + + if (currentHash.startsWith(difficulty)) { + setNonce(miningNonce); + setIsMining(false); + break; + } + + miningNonce++; + // Update the UI periodically to avoid freezing the browser + if (miningNonce % 100 === 0) { + setNonce(miningNonce); + await new Promise(resolve => setTimeout(resolve, 0)); // Yield to the browser + } + } + }; + + mine(); + + return () => { + continueMining = false; + } + }, [isMining, challenge, nonce, difficulty]); + + + const handleMineClick = () => { + setIsMining(true); + } + + const handleStopClick = () => { + setIsMining(false); + } + + const handleResetClick = () => { + setIsMining(false); + setNonce(0); + } + + const handleNewChallengeClick = () => { + setIsMining(false); + setChallenge(generateRandomHex(16)); + setNonce(0); + } + + // Helper to render the hash with colored leading characters + const renderHash = () => { + if (!hash) return ...; + const prefix = hash.substring(0, difficulty.length); + const suffix = hash.substring(difficulty.length); + const prefixColor = isFound ? styles.hashPrefixGreen : styles.hashPrefixRed; + return ( + <> + {prefix} + {suffix} + + ); + }; + + return ( +
+
+
+ {/* Challenge Block */} +
+

1. Challenge

+

{challenge}

+
+ + {/* Nonce Control Block */} +
+

2. Nonce

+
+ + {nonce} + +
+
+ + {/* Combined Data Block */} +
+

3. Combined Data

+

{combinedData}

+
+
+ + {/* Arrow pointing down */} +
+ + + +
+ + {/* Hash Output Block */} +
+
+
+

4. Resulting Hash (SHA-256)

+

{renderHash()}

+
+
+ {isFound ? : } +
+
+
+ + {/* Mining Controls */} +
+ {!isMining ? ( + + ) : ( + + )} + + +
+
+
+ ); +} diff --git a/docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/styles.module.css b/docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/styles.module.css new file mode 100644 index 0000000..4bf391c --- /dev/null +++ b/docs/blog/2025-08-28-cpu-core-odd/ProofOfWorkDiagram/styles.module.css @@ -0,0 +1,317 @@ +/* Main container styles */ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: white; + font-family: ui-sans-serif, system-ui, sans-serif; + margin-top: 2rem; + margin-bottom: 2rem; +} + +.innerContainer { + width: 100%; + max-width: 56rem; + margin: 0 auto; +} + +/* Header styles */ +.header { + text-align: center; + margin-bottom: 2.5rem; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + color: rgb(34 211 238); +} + +.subtitle { + font-size: 1.125rem; + color: rgb(156 163 175); + margin-top: 0.5rem; +} + +/* Grid layout styles */ +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + align-items: center; + text-align: center; +} + +/* Block styles */ +.block { + background-color: rgb(31 41 55); + padding: 1.5rem; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; +} + +.blockTitle { + font-size: 1.125rem; + font-weight: 600; + color: rgb(34 211 238); + margin-bottom: 0.5rem; +} + +.challengeText { + font-size: 0.875rem; + color: rgb(209 213 219); + word-break: break-all; + font-family: ui-monospace, SFMono-Regular, monospace; +} + +.combinedDataText { + font-size: 0.875rem; + color: rgb(156 163 175); + word-break: break-all; + font-family: ui-monospace, SFMono-Regular, monospace; +} + +/* Nonce control styles */ +.nonceControls { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; +} + +.nonceButton { + background-color: rgb(55 65 81); + border-radius: 9999px; + padding: 0.5rem; + transition: background-color 200ms; +} + +.nonceButton:hover:not(:disabled) { + background-color: rgb(34 211 238); +} + +.nonceButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.nonceValue { + font-size: 1.5rem; + font-family: ui-monospace, SFMono-Regular, monospace; + width: 6rem; + text-align: center; +} + +/* Icon styles */ +.icon { + height: 2rem; + width: 2rem; +} + +.iconGreen { + height: 2rem; + width: 2rem; + color: rgb(74 222 128); +} + +.iconRed { + height: 2rem; + width: 2rem; + color: rgb(248 113 113); +} + +.iconSmall { + height: 1.5rem; + width: 1.5rem; +} + +.iconGray { + height: 2.5rem; + width: 2.5rem; + color: rgb(75 85 99); + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* Arrow animation */ +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.arrowContainer { + display: flex; + justify-content: center; + margin: 1.5rem 0; +} + +/* Hash output styles */ +.hashContainer { + padding: 1.5rem; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + transition: all 300ms; + border: 2px solid; +} + +.hashContainerSuccess { + background-color: rgb(20 83 45 / 0.5); + border-color: rgb(74 222 128); +} + +.hashContainerError { + background-color: rgb(127 29 29 / 0.5); + border-color: rgb(248 113 113); +} + +.hashContent { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; +} + +.hashText { + text-align: center; +} + +.hashTextLg { + text-align: left; +} + +.hashValue { + font-size: 0.875rem; + word-break: break-all; +} + +.hashValueLg { + font-size: 1rem; + word-break: break-all; +} + +.hashIcon { + margin-top: 1rem; +} + +.hashIconLg { + margin-top: 0; +} + +/* Hash highlighting */ +.hashPrefix { + font-family: ui-monospace, SFMono-Regular, monospace; +} + +.hashPrefixGreen { + color: rgb(74 222 128); +} + +.hashPrefixRed { + color: rgb(248 113 113); +} + +.hashSuffix { + font-family: ui-monospace, SFMono-Regular, monospace; + color: rgb(156 163 175); +} + +/* Button styles */ +.buttonContainer { + margin-top: 2rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; +} + +.button { + font-weight: 700; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + transition: transform 150ms; +} + +.button:hover { + transform: scale(1.05); +} + +.buttonCyan { + background-color: rgb(8 145 178); + color: white; +} + +.buttonCyan:hover { + background-color: rgb(6 182 212); +} + +.buttonYellow { + background-color: rgb(202 138 4); + color: white; +} + +.buttonYellow:hover { + background-color: rgb(245 158 11); +} + +.buttonIndigo { + background-color: rgb(79 70 229); + color: white; +} + +.buttonIndigo:hover { + background-color: rgb(99 102 241); +} + +.buttonGray { + background-color: rgb(55 65 81); + color: white; +} + +.buttonGray:hover { + background-color: rgb(75 85 99); +} + +/* Responsive styles */ +@media (min-width: 768px) { + .title { + font-size: 3rem; + } + + .grid { + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + } + + .hashContent { + flex-direction: row; + } + + .hashText { + text-align: left; + } + + .hashValue { + font-size: 1rem; + } + + .hashIcon { + margin-top: 0; + } +} + +@media (max-width: 767px) { + .grid { + display: flex; + flex-direction: column; + gap: 1rem; + } +} diff --git a/docs/blog/2025-08-28-cpu-core-odd/index.mdx b/docs/blog/2025-08-28-cpu-core-odd/index.mdx new file mode 100644 index 0000000..d12abb6 --- /dev/null +++ b/docs/blog/2025-08-28-cpu-core-odd/index.mdx @@ -0,0 +1,129 @@ +--- +slug: 2025/cpu-core-odd +title: Sometimes CPU cores are odd +description: "TL;DR: all the assumptions you have about processor design are wrong and if you are unlucky you will never run into problems that users do through sheer chance." +authors: [xe] +tags: + - bugfix + - implementation +image: parc-dsilence.webp +--- + +import ProofOfWorkDiagram from "./ProofOfWorkDiagram"; + +![](./parc-dsilence.webp) + +One of the biggest lessons that I've learned in my career is that all software has bugs, and the more complicated your software gets the more complicated your bugs get. A lot of the time those bugs will be fairly obvious and easy to spot, validate, and replicate. Sometimes, the process of fixing it will uncover your core assumptions about how things work in ways that will leave you feeling like you just got trolled. + +Today I'm going to talk about a single line fix that prevents people on a large number of devices from having weird irreproducible issues with Anubis rejecting people when it frankly shouldn't. Stick around, it's gonna be a wild ride. + +{/* truncate */} + +## How this happened + +Anubis is a web application firewall that tries to make sure that the client is a browser. It uses a few [challenge methods](/docs/admin/configuration/challenges/) to do this determination, but the main method is the [proof of work](/docs/admin/configuration/challenges/proof-of-work/) challenge which makes clients grind away at cryptographic checksums in order to rate limit clients from connecting too eagerly. + +:::note + +In retrospect implementing the proof of work challenge may have been a mistake and it's likely to be supplanted by things like [Proof of React](https://github.com/TecharoHQ/anubis/pull/1038) or other methods that have yet to be developed. Your patience and polite behaviour in the bug tracker is appreciated. + +::: + +In order to make sure the proof of work challenge screen _goes away as fast as possible_, the [worker code](https://github.com/TecharoHQ/anubis/tree/main/web/js/worker) is optimized within an inch of its digital life. One of the main ways that this code is optimized is with how it's run. Over the last 10-20 years, the main way that CPUs have gotten fast is via increasing multicore performance. Anubis tries to make sure that it can use as many cores as possible in order to take advantage of your device's CPU as much as it can. + +This strategy sometimes has some issues though, for one Firefox seems to get _much slower_ if you have Anubis try to absolutely saturate all of the cores on the system. It also has a fairly high overhead between JavaScript JIT code and [WebCrypto](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). I did some testing and found out that Firefox's point of diminishing returns was about half of the CPU cores. + +## Another "invalid response" bug + +One of the complaints I've been getting from users and administrators using Anubis is that they've been running into issues where users get randomly rejected with an error message only saying "invalid response". This happens when the challenge validating process fails. This issue has been blocking the release of the next version of Anubis. + +In order to demonstrate this better, I've made a little interactive diagram for the proof of work process: + + + +I've fixed a lot of the easy bugs in Anubis by this point. A lot of what's left is the hard bugs, but also specifically the kinds of hard bugs that involve weird hardware configurations. In order to try and catch these issues before software hits prod, I test Anubis against a bunch of hardware I have locally. Any issues I find and fix before software ships are issues that you don't hit in production. + +Let's consider [the line of code](https://github.com/TecharoHQ/anubis/blob/main/web/js/algorithms/fast.mjs) that was causing this issue: + +```js +threads = Math.max(navigator.hardwareConcurrency / 2, 1), +``` + +This is intended to make your browser spawn a proof of work worker for _half_ of your available CPU cores. If you only have one CPU core, you should only have one worker. Each thread is given this number of threads and uses that to increment the nonce so that each thread doesn't try to find a solution that another worker has already performed. + +One of the subtle problems here is that all of the parts of this assume that the thread ID and nonce are integers without a decimal portion. Famously, [all JavaScript numbers are IEEE 754 floating point numbers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number). Surely there wouldn't be a case where the thread count could be a _decimal_ number, right? + +Here's all the devices I use to test Anubis _and their core counts_: + +| Device Name | Core Count | +| :--------------------------- | :--------- | +| MacBook Pro M3 Max | 16 | +| MacBook Pro M4 Max | 16 | +| AMD Ryzen 9 7950x3D | 32 | +| Google Pixel 9a (GrapheneOS) | 8 | +| iPhone 15 Pro Max | 6 | +| iPad Pro (M1) | 8 | +| iPad mini | 6 | +| Steam Deck | 8 | +| Core i5 10600 (homelab) | 12 | +| ROG Ally | 16 | + +Notice something? All of those devices have an _even_ number of cores. Some devices such as the [Pixel 8 Pro](https://www.gsmarena.com/google_pixel_8_pro-12545.php) have an _odd_ number of cores. So what happens with that line of code as the JavaScript engine evaluates it? + +Let's replace the [`navigator.hardwareConcurrency`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/hardwareConcurrency) with the Pixel 8 Pro's 9 cores: + +```js +threads = Math.max(9 / 2, 1), +``` + +Then divide it by two: + +```js +threads = Math.max(4.5, 1), +``` + +Oops, that's not ideal. However `4.5` is bigger than `1`, so [`Math.max`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) returns that: + +```js +threads = 4.5, +``` + +This means that each time the proof of work equation is calculated, there is a 50% chance that a valid solution would include a nonce with a decimal portion in it. If the client finds a solution with such a nonce, then it would think the client was successful and submit the solution to the server, but the server only expects whole numbers back so it rejects that as an invalid response. + +I keep telling more junior people that when you have the weirdest, most inconsistent bugs in software that it's going to boil down to the dumbest possible thing you can possibly imagine. People don't believe me, then they encounter bugs like this. Then they suddenly believe me. + +Here is the fix: + +```js +threads = Math.trunc(Math.max(navigator.hardwareConcurrency / 2, 1)), +``` + +This uses [`Math.trunc`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc) to truncate away the decimal portion so that the Pixel 8 Pro has `4` workers instead of `4.5` workers. + +## Today I learned this was possible + +This was a total "today I learned" moment. I didn't actually think that hardware vendors shipped processors with an odd number of cores, however if you look at the core geometry of the Pixel 8 Pro, it has _three_ tiers of processor cores: + +| Core type | Core model | Number | +| :----------------- | :------------------- | :----- | +| High performance | 3 Ghz Cortex X3 | 1 | +| Medium performance | 2.45 Ghz Cortex A715 | 4 | +| High efficiency | 2.15 Cortex A510 | 4 | +| Total | | 9 | + +I guess every assumption that developers have about CPU design is probably wrong. + +This probably isn't helped by the fact that for most of my career, the core count in phones has been largely irrelevant and most of the desktop / laptop CPUs I've had (where core count does matter) uses [simultaneous multithreading](https://en.wikipedia.org/wiki/Simultaneous_multithreading) to "multiply" the core count by two. + +The client side fix is a bit of an "emergency stop" button to try and mitigate the badness as early as possible. In general I'm quite aware of the terrible UX involved with this flow failing and I'm still noodling through ways to make that UX better and easier for users / administrators to debug. + +I'm looking into the following: + +1. This could have been prevented on the server side by doing less strict input validation in compliance with [Postel's Law](https://en.wikipedia.org/wiki/Robustness_principle). I feel nervous about making such a security-sensitive endpoint _more liberal_ with the inputs it can accept, but it may be fine? I need to consult with a security expert. +2. Showing an encrypted error message on the "invalid response" page so that the user and administrator can work together to fix or report the issue. I remember Google doing this at least once, but I can't recall where I've seen it in the past. Either way, this is probably the most robust method even though it would require developing some additional tooling. I think it would be worth it. + +I'm likely going to go with the second option. I will need to figure out a good flow for this. It's likely going to involve [age](https://github.com/FiloSottile/age). I'll say more about this when I have more to say. + +In the meantime though, looks like I need to expense a used Pixel 8 Pro to add to the testing jungle for Anubis. If anyone has a deal out there, please let me know! + +Thank you to the people that have been polite and helpful when trying to root cause and fix this issue. diff --git a/docs/blog/2025-08-28-cpu-core-odd/parc-dsilence.webp b/docs/blog/2025-08-28-cpu-core-odd/parc-dsilence.webp new file mode 100644 index 0000000000000000000000000000000000000000..4d4717525e0051659f740b778e838af382155cae GIT binary patch literal 18908 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