VOOZH about

URL: https://pablotron.org/

⇱ pablotron.org


polycvss v0.4.0

June 28, 2026

I just released polycvss v0.4.0.

polycvss is a Rust library to parse and score CVSS vector strings.

Changes since polycvss v0.3.5:

  • derive Hash for Vector, Name, Group, MajorVersion, Version, Severity
  • add Err::InvalidChar, check for invalid characters when parsing vector strings
  • remove unused TryFrom<String>
  • add fuzz targets
  • increase coverage to 99%
  • README.md: add Linter, Coverage, and Fuzzing sections

Links

GitHub Enshittification

April 30, 2026

This time it'll be different...

Recent GitHub problems:

Gentoo and Ghostly are migrating away from GitHub; the former because of the aggressive Copilot marketing, and the latter because of the repeated outages.

Microsoft has apologized and vowed to do better, but I think GitHub will continue to deteriorate.

Why?

  1. Microsoft has made pledges like this before. In 2024 after a series of high-profile security blunders, a Microsoft representative said “We are making security our top priority” and announced an initiative which made executive pay partially dependent on meeting security plans and milestones. Despite that assurance, in 2025 Microsoft was criticized by Senator Ron Wyden for “dangerous software engineering decisions” and in 2026 poor Microsoft cloud security was at the center of a scathing ProPublica investigation.

  2. The decline of GitHub was anticipated when Microsoft acquired GitHub in 2018 and became almost inevitable once Microsoft folded GitHub into their CoreAI division last year. As long as they are incentivized to do so, Microsoft employees will continue to prioritize aggressive Copilot marketing over any other concern, including platform reliability.

  3. Many of the problems listed above can be traced directly to deliberate changes made by Microsoft in pursuit of their disasterous, money-losing “AI” initiative. Degrading the quality of a platform to maximize short-term shareholder profit is textbook enshittification 2.

So what should we do?

I don’t know exactly, but here are some suggestions:

  • Developers: Consider migrating your open source and personal projects to Codeberg, another development platform, or a self-hosted Forgejo instance. After migrating, either a) archive the old GitHub repositories or b) maintain the old GitHub repositories as read-only mirrors of the new repositories. You can use the git-remote command for a code-only repository migration or use Codeberg’s repository migration tool if you want to migrate a repository that has issues, releases, or a wiki.
  • Package Registries (crates.io, pypi.org, etc): Make sure that your registry supports packages hosted on other platforms and that it allows developers to authenticate using a non-GitHub identity. Most registries do the former, but many don’t do the latter (I’m looking at you, crates.io).
  • Packaging Tools (cargo, pip, etc): Make sure there is an easy and well-documented way to publish packages for projects hosted on non-GitHub platforms.
  • CI/CD Tools: Provide documentation on integrating your tool on a non-GitHub platform and/or make your tool available as a container.

I suspect GitHub’s problems will compound as the “AI” bubble begins to collapse. Unfortunately I don’t know if that means “next month” or “next year” 3.

Regardless, now seems like a good time to proceed towards the exit. I’m going to start migrating my personal projects to Codeberg.

Updates


  1. Technically you can opt-out of the LLM training and CLI telemetry if you a) know that it is happening and b) can find the correct setting, but collecting users’ data without their knowledge or explicit consent is a deceptive design pattern known as Privacy Zuckering↩︎ ↩︎

  2. We have seen this pattern of decay on other development platforms too. In 2013 SourceForge added an optional feature to place ad-supported content into binary installers in a dubious attempt at generating revenue, and by 2015 they were hijacking project pages and adding adware and malware to project downloads without developers' consent↩︎

  3. For more information I recommend following Ed Zitron; his reporting is detailed and his predictions about the “AI” bubble have been solid. ↩︎

Forgejo Setup

April 23, 2026

Forgejo runners.

Several months ago I set up a personal Forgejo server and several runners. I have been using it to manage releases for a couple of projects.

I documented the setup process here: Forgejo Setup.

Update (2026-04-26): Setup guide updated for Forgejo v15.0.

Stupid Slop Scrapers Sink Site

March 30, 2026

This morning I woke up to an oddly unresponsive web server. The TLS handshake would complete and then requests would block until the connection timed out.

At first I thought it was a transient error, so I restarted Apache. That didn’t fix the problem, so I restarted the VM, and then the physical machine.

The cause was a slop bot flood; apparently they discovered git.pablotron.org and decided to blunderbuss the server by simultaneously scraping the site.

git.pablotron.org is (was) a cgit instance with a relatively lax robots.txt. cgit is extremely fast, but it still generates content dynamically. This means that git.pablotron.org is (was) susceptible to what is effectively a DDoS attack.

I tried tweaking Apache parameters but that didn’t help, so I’ve shut git.pablotron.org down for now. I’ll bring it back once I have time to add some slop mitigation.

Fortunately pablotron.org itself is less vulnerable to this kind of abuse because it is statically generated and has a restrictive robots.txt.

Update (2026-03-30): Improve wording.

Disable Firefox AI Slop

March 28, 2026

I restarted Firefox after an update and my laptop fan started going berzerk.

The culprit was a background thread pegged at 100% CPU for one of the new AI “features” in Firefox; presumably doing inference for a local model.

I disabled this junk, and you should too.

Steps:

  1. Go to Settings.
  2. Click “AI Controls” on the left sidebar.
  3. Click the “Block AI enhancements” radio button.
  4. Set all of the drop-down menus to “Blocked” in the “On-device AI” section.
  5. Set all of the drop-down menus to “Blocked” in the “AI chatbot providers in sidebar” section.
  6. (Optional) Send Mozilla a nastygram. Let them know how you feel about features which waste your CPU, battery, disk space, and time for no discernable benefit. Profanity encouraged!

This absurd gaggle of misfeatures should have always been opt-in, not opt-out.

I also resent the Orwellian doublespeak that Mozilla is using to describe this nonsense.

Definitions:

  • opt-in: A feature which is disabled by default. The user must take an explicit action to enable the feature.
  • opt-out: A feature which is enabled by default. The user must take an explicit action to disable the feature.

The intentional obfuscation suggests that Mozilla was aware it was inappropriate to enable this claptrap by default.

George Lakoff wrote about the use of Orwellian language in politics in “Don’t Think of an Elephant!”:

But we should recognize that they use Orwellian language precisely when they have to: when they are weak, when they cannot just come out and say what they mean. Imagine if they came out supporting a “Dirty Skies Bill” or a “Forest Destruction Bill” or a “Kill Public Education” bill. They would lose. They are aware people do not support what they are really trying to do.

Orwellian language points to weakness – Orwellian weakness. When you hear Orwellian language, note where it is, because it is a guide to where they are vulnerable.

pbech32 v0.1.0

March 8, 2026

I just released the first version of pbech32, a Rust library for encoding and decoding Bech32 data.

Bech32 is a fast and user-friendly base 32 encoding format that includes a namespace and checksum.

Links

Library Features

Examples

Decode from string:

usepbech32::Bech32;lets="a1qypqxpq9mqr2hj";// bech32m-encoded string
letgot: Bech32=s.parse()?;// decode string
assert_eq!(got.hrp.to_string(),"a");// check human-readable part
assert_eq!(got.data,vec![1,2,3,4,5]);// check data

Encode to string:

usepbech32::{Bech32,Hrp,Scheme};letscheme=Scheme::Bech32m;// checksum scheme
lethrp: Hrp="a".parse()?;// human-readable part
letdata=vec![1,2,3,4,5];// data
letgot=Bech32{scheme,hrp,data}.to_string();// encode as string
assert_eq!(got,"a1qypqxpq9mqr2hj");// check result

Encode to a writer:

usestd::io::Write;usepbech32::{Encoder,Hrp,Scheme};letmutvec: Vec<u8>=Vec::new();// output vector
lethrp: Hrp="hello".parse()?;// human readable part
{letmutenc=Encoder::new(&mutvec,Scheme::Bech32m,hrp)?;// create encoder
enc.write_all(b"folks")?;// write data
enc.flush()?;// flush encoder (RECOMMENDED)
}letgot=str::from_utf8(vec.as_ref())?;// convert output vector to string
assert_eq!(got,"hello1vehkc6mn27xpct");// check result

More examples are available in the examples/ directory of the pbech32 Git repository.

Updates

  • 2026-03-27: pbech32 v0.2.0: Add impl Drop for Encoder. Fix clippy warnings. Minor documentation improvements.

New Year's Primality Testing by Hand

January 2, 2026

Happy New Year!

The number 2026 has two factors: 2 and 1013. I wondered “is 1013 prime?” and, for fun, “can I solve this in my head?” (e.g. no calculator, computer, or pen and paper).

If 1013 is not prime, then it is composite and must have at least one odd prime factor ≤ ⌊√1013⌋.

(We know the factor – if it exists – is odd because factors of odd composites are always odd, and we know it is prime because of the fundamental theorem of arithmetic).

So our approach will be to check odd primes from 3 to √1013 to see if any divide 1013.

Unfortunately we don’t know √1013. Fortunately 1013 is close to 1024, and 1024 is even power of 2. So let’s use 1024 to approximate √1013:

  1. 1013 < 1024, so √1013 < √1024
  2. √1024 = √(210 ) = 25 = 32
  3. Therefore √1013 < 32

So we need to test odd primes in the range [3,31] to see if any divide 1013.

Before that, though, we prune the list of potential factors with the divisibility rules. We remove:

  • 3, because the sum of the digits of 1013 isn’t divisible by 3: 1+1+3=5 and 3∤5
  • 5, because the last digit of 1013 isn’t 0 or 5.
  • 7, because 5 times the last digit (3) plus the rest (101) is not a multiple of 7: 5*3+101=116, 5*6+11=41, and 7∤41.
  • …and so on for 11, 13, 17, 19, 23, and 29.

(I didn’t remember the divisibility rules for the range [11,29], so I checked if any of the primes divide 1013 instead).

31 is the only remaining potential factor after pruning. To check it, we can either use the Euclidean algorithm to see if gcd(31, 1013) != 1 or do some trial arithmetic. I chose the latter:

  1. 31 = (30 + 1)
  2. (30 + 1) * 33 = 990 + 33 = 1023
  3. The closest multiples of 31 are 992 and 1023, so 31∤1013.

1013 does not have any odd prime factors in the range [3,√1013], so it must be prime.

Let’s check our work with SymPy:

>>> import sympy
>>> sympy.ntheory.primetest.isprime(1013)
True

Success!

Further Reading

polycvss v0.2.0

October 4, 2025

I just released polycvss version 0.2.0.

polycvss is a Rust library to parse and score CVSS vector strings.

Features:

  • CVSS v2, CVSS v3, and CVSS v4 support.
  • Version-agnostic parsing and scoring API.
  • Memory efficient: Vectors are 8 bytes. Scores and severities are 1 byte.
  • No dependencies by default except the standard library.
  • Optional serde integration via the serde build feature.
  • Extensive tests: Tested against thousands of vectors and scores from the NVD CVSS calculators.

Here is an example tool which parses the first command-line argument as a CVSS vector string, then prints the score and severity:

usepolycvss::{Err,Score,Severity,Vector};fn main()-> Result<(),Err>{letargs: Vec<String>=std::env::args().collect();// get cli args
ifargs.len()==2{letvec: Vector=args[1].parse()?;// parse string
letscore=Score::from(vec);// get score
letseverity=Severity::from(score);// get severity
println!("{score}{severity}");// print score and severity
}else{letname=args.first().map_or("app",|s|s);// get app name
eprintln!("Usage: {name} [VECTOR]");// print usage
}Ok(())}

Here is the example tool output for a CVSS v2 vector string, a CVSS v3 vector string, and a CVSS v4 vector string:

# test with cvss v2 vector string
$ cvss-score "AV:A/AC:H/Au:N/C:C/I:C/A:C"
6.8 MEDIUM
# test with cvss v3 vector string
$ cvss-score "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
9.8 CRITICAL
# test with cvss v4 vector string
$ cvss-score "CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:P/VC:L/VI:L/VA:L/SC:H/SI:H/SA:H"
5.2 MEDIUM

This example tool is included in the Git repository as src/bin/cvss-score.rs.

Links

Updates

  • 2025-10-12: polycvss v0.2.1: Add polycvss::v4::Nomenclature and improve documentation.
  • 2025-10-18: polycvss v0.3.0: Add user-friendly Error messages, remove unreleased CVSS v2.x Version variants, and improve documentation.
  • 2025-10-19: polycvss v0.3.1: Documentation improvements.
  • 2025-11-16: polycvss v0.3.2: Add impl From<Vector> for Severity and examples/ directory.
  • 2026-02-01: polycvss v0.3.3: Add v4-scores example, update dependencies, documentation and formatting fixes.
  • 2026-03-07: polycvss v0.3.4: Update dependencies and copyright year. Minor documentation improvements.
  • 2026-03-27: polycvss v0.3.5: Increase test coverage to 98%. Add impl std::error::Error for polycvss::Err. Remove unused code. Minor documentation fixes and improvements.
  • 2026-06-28: polycvss v0.4.0: Increase test coverage to 99%. Derive Hash for many types. Add Err::InvalidChar and check for invalid characters when parsing vector strings. Add fuzz targets. Documentation fixes and improvements.

Armbian on Odroid N2L

June 8, 2025

Last week I installed Armbian on an Odroid N2L. The installation steps, installation results, and fixes for some problems are documented below.

Installation

  1. Download and import the signing key (fingerprint DF00FAF1C577104B50BF1D0093D6889F9F0E78D5):
    wget -O- https://apt.armbian.com/armbian.key | gpg -- import -
  2. Download the current “Debian 12 (Bookworm)” image and the detached signature from the “Minimal/IOT images” section of the Armbian Odroid N2L page.
  3. Verify the signature:
    gpg --verify Armbian_community_25.8.0-trunk.8_Odroidn2l_bookworm_current_6.12.28_minimal.img.xz{.asc,}
  4. Uncompress the image:
    unxz Armbian_community_25.8.0-trunk.8_Odroidn2l_bookworm_current_6.12.28_minimal.img.xz
  5. Flash the uncompressed image to a microSD card:
    sudo dd if=Armbian_community_25.8.0-trunk.8_Odroidn2l_bookworm_current_6.12.28_minimal.img of=/dev/sda bs=1M status=progress
  6. Mount the second partition of the microSD card on /mnt/tmp:
    sudo mount /dev/sda2 /mnt/tmp
  7. Use the instructions and template from Automatic first boot configuration to populate /mnt/tmp/root/.not_logged_in_yet. My populated autoconfig is here, but it did not work as expected; see below.
  8. Unmount the second partition of the microSD card.
  9. Insert the microSD card into the Odroid N2L and power it on.

Installation Results

Worked as expected:

  • Successfully booted.
  • Successfully connected to WiFi on first boot.

Did not work as expected:

  • Did not connect to WiFI on subsequent boots.
  • Did not set the root password. Instead the root password was 1234.
  • Did not set the user password.
  • Did not set the user SSH key.

Fixes

To correct these problems I connected a keyboard and monitor and did the following:

  1. Logged in as root with the password 1234.
  2. Changed the root password and the user password.
  3. Edited /etc/netplan/20-eth-fixed-mac.yaml and fixed the errors. The corrected version is below.
  4. Ran netplan apply to apply the corrected network configuration.
  5. Rebooted to confirm that networking was working as expected.

Here is the corrected /etc/netplan/20-eth-fixed-mac.yaml:

network:version:2

After fixing networking, I did the following:

  1. Copied my SSH key.
  2. Edited /etc/ssh/sshd_config to disable root logins and password logins.
  3. Ran apt-get update && apt-get upgrade.
  4. Installed unattended-upgrades.
  5. Rebooted to pick up the latest updates.

Odroid N2L running Armbian.

Nginx Caching and Security Headers

June 8, 2025

Yesterday I ported the caching and security headers from the Apache configuration for the public site to the Nginx
configuration for the Tor mirror.

The caching headers are particularly helpful for the Tor mirror.

The updated Nginx configuration and additional documentation are here: Site Backend - Onion Service.

Archived Posts...
© 1998-2026 Paul Duncan