<![CDATA[SSG's]]>https://ssg.dev/https://ssg.dev/favicon.pngSSG'shttps://ssg.dev/Ghost 6.22Sun, 15 Mar 2026 08:00:23 GMT60<![CDATA[The forsaken world of Windows Task Scheduler]]>https://ssg.dev/the-forsaken-world-of-windows-task-scheduler/69b46b546dabfc000123012eFri, 13 Mar 2026 22:17:30 GMTPart 1: The failing taskThe forsaken world of Windows Task Scheduler

One day, I decided to write a backup script that should run every morning. That was how my adventures began.

My task was simple. It was a copy operation from a local folder to NAS for my source code. Just a batch file. The trigger was also simple. Run in the morning, and wake the PC up if necessary, which is a great feature by the way.

The forsaken world of Windows Task Scheduler

I'm sure there's a bash script for cron that lets Linux do that by stringing together three different tools, that might exist on a given distro or not, to perform that, but having this checkbox on every Windows machine is extremely convenient, I must say. That's what user-friendliness is all about.

I noticed one day that my backups weren't working at all by chance because the scheduled task had been returning success all that time. I bumped into my backup folder and noticed that files that were supposed to be there were missing. I found out that, I had a line in the script that I used to filter extraneous strings to make debugging easier similar to grep -v:

robocopy ... | findstr /v "*EXTRA"

The problem was that robocopy's exit code was overriden by findstr's. So, I fixed that, and after the fix, I noticed that the task wasn't working at all because the network share was inaccessible. Why? Because I wanted task to run despite that I was logged in or not at the time.

The forsaken world of Windows Task Scheduler

Again, another great user-friendly feature. I don't even know how you'd go about doing that on Linux. Probably some systemctl calls, some mock services, who knows. Again, it's just one click on Windows, great.

But, why wasn't it working? Because, I had no password. Why did I have no password? Because, my account was forced to be linked to my Microsoft account, and my Microsoft account was strongly recommended to be made "passwordless" for great security, remember? Great, now I have no password that I could store in Task Scheduler.

So, different teams at Microsoft had different goals and priorities that basically ended up making my life miserable.

I disconnected my Windows account from my Microsoft account, set a password. Now, I was able to schedule the task without me being logged on. Yay, I guess. The problem is, now Windows is bothering me from all fronts: "connect your Microsoft account! You're a terrible person for not doing that! Microsoft account is your salvation!". Actually, there are few benefits of having your Microsoft account linked too, like synchronizing settings across devices, so I don't mind it. And, I don't want to be bothered either, so I thought "hey I set this once, maybe it will stay up after I connect", I linked my Microsoft account again. My task started failing again immediately.

I simply just can't schedule tasks to run on my machine unattended if my account is linked to my Microsoft account.

There's only one thing to do: create another account. That's what I did. I created a local account just for this purpose. Tried to schedule the task using that account, but failed again.

The forsaken world of Windows Task Scheduler
Ouch

It turns out adding the account isn't enough. You must add it to some obscure policy in some obscure UI called Group Policy Editor.

The forsaken world of Windows Task Scheduler

And that fixed it. I now have a redundant account that has the same privileges as my Microsoft account, but with a password. So, I practically don't have any benefits of having a passwordless account now, because another account with a password can already access everything on my machine. Why were we doing this again?

Anyway. That finally worked out, but probably took more time than stringing together several Linux scripts.

But it wasn't enough. Remember, the first time I noticed there was a problem, it was by pure chance. Nothing was warning me about the failure. I was sure that Microsoft had thought of that mechanism, to inform me when a task fails. And they did:

The forsaken world of Windows Task Scheduler

Ouch again! "Deprecated". It would have worked great, but I wasn't going to rely on something that Microsoft said "hey don't rely on this anymore" about.

I could have used a PowerShell script perhaps? I'm sure PowerShell people thought of simply sending an email? And yes, they had Send-MailMessage cmdlet built-in on Windows, which is great, but...

The forsaken world of Windows Task Scheduler

This time "obsolete", not even deprecated. Email is out of fashion I guess. There was no proper, built-in way to send an email. So, I thought of using my Home Assistant setup as a way to notify myself for potential failures.

I wrote a script to notify myself whenever a scheduled task fails. I scheduled it on Task Scheduler itself too, hoping that it won't fail and put itself in an infinite loop. The trigger part looked like this:

  <Triggers>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Microsoft-Windows-TaskScheduler/Operational"&gt;&lt;Select Path="Microsoft-Windows-TaskScheduler/Operational"&gt;*[System[(Level=2) and TimeCreated[timediff(@SystemTime) &amp;lt;= 5000]] and EventData[starts-with(Data[@Name='TaskName'],'\me\')]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
  </Triggers>

It basically triggers every time an error in the Task Scheduler event log is detected. Whenever it's triggered, it would send a notification to my Home Assistant instance.

The problem is, normally, Windows doesn't even log Task Scheduler failures. I don't know why. Are scheduled tasks unimportant? Are they expected to fail? Why do they exist if so? In order to enable Task Scheduler failure logging you need to enable the relevant event log using this command:

wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:true

Every scheduled task failure would now be logged under the path Applications and Services Logs > Microsoft > Windows > TaskScheduler > Operational. And guess what, it worked!

The forsaken world of Windows Task Scheduler
("pls cnm ltf" is a mock white collar speak in Turkish for "please")

Part 2: The failing everything

Now, I had a way to know about scheduled task errors. But as a side-effect, I started learning about ALL scheduled task errors, not just mine. Scheduled tasks were constantly failing all the time.

The forsaken world of Windows Task Scheduler

When I checked Task Scheduler's event log I saw errors everywhere.

The forsaken world of Windows Task Scheduler

Those were completely irrelevant, arbitrary, random tasks. I could change my monitoring task to trigger only for my scheduled tasks, but I got curious and wanted to investigate them all.

One pattern I noticed was that many tasks used a COM component either for a trigger for its action, but its DLL wasn't registered on my Windows system. An example is like this:

  <Actions Context="LocalSystem">
    <ComHandler>
      <ClassId>{07369A67-07A6-4608-ABEA-379491CB7C46}</ClassId>
    </ComHandler>
  </Actions>

That's an example I got from a working task. But, the idea is the same for failing ones too. The ClassId points to a component in the registry and shows where the component can be instantiated from.

The forsaken world of Windows Task Scheduler

Interestingly, in all my investigation attempts, the DLL was there, but Windows had no way of knowing how to use it. I tried to register it myself using regsvr32, but that also failed. I have no idea why. Maybe, COM has changed since I last used it, or maybe that component hasn't been functional for a long time, and the file was left there for compatibility reasons?

Then, why was the task scheduled? I presume because nobody cared if it failed silently as all scheduled task failures are silent by default. But, that meant, Task Scheduler had to do a failing COM resolution, despite that it's a simple registry lookup, every time that task needed to be run unnecessarily.

That wasn't one task. There were at least a handful that was failing for the exact same reason: not-working COM component.

After a certain point, I got bored of those missing COM registration issues, I didn't want those tasks constantly triggering COM lookups, filling up event log, and notifying me, so I wanted to disable the scheduled task entries. I couldn't.

The forsaken world of Windows Task Scheduler

Well, the thing is, scheduled task entries are just files on disk. They are under C:\Windows\System32\Tasks in the same tree structure shown on the Task Scheduler UI. I went ahead and simply deleted the files. How about them apples, Task Scheduler?

Obviously, Windows wasn't going to let me go by without punishment. Now, I would receive errors for tasks not existing.

The forsaken world of Windows Task Scheduler

Since task itself didn't exist, what did trigger this task? It still showed up in the scheduled task list. How did that happen? I didn't know. Grepping its name has found nothing. Then I scanned the registry. And it turns out that Task Scheduler keeps a copy of tasks in two separate places in registry too, for good measure I guess, under these keys:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree

You need to remove the tasks from those places too in order for Task Scheduler to completely ignore them. I was able to do that. No more "noo, we won't allow you to do that".

Luckily, there's a PowerShell cmdlet that can remove a task safely from all those places called Unregister-ScheduledTask. Its syntax is a bit backwards, but you can remove a task fully by just typing:

Unregister-ScheduledTask -TaskPath "\Microsoft\Windows\DeviceDirectoryClient\" -TaskName "RegisterUserDevice"

and you don't get any "you can't do that" errors this way.

Obviously, unregistered COM components aren't the only sources of errors. There are many varying errors in Task Scheduler, like tasks failing for no reason with an obscure E_FAIL (0x80004005) which basically means "unknown error", a term I've always found strange. If you don't know what kind of error it is, how do you know if it's an error? Maybe, it's an unknown success?

Task Scheduler feels more like an iceberg the more I dig deeper into investigating its issues. There are lots of orphaned, forgotten, not working tasks tirelessly trying to wake up, spin, and fail. It reminds me of abandoned robots in an extremely cute indie game I'd been playing recently called Mashina. It's pretty much obvious that Task Scheduler errors are considered negligible, probably because users aren't aware of them thanks to the rug that they were swept under. But, I think a good functioning system needs all of its parts working in harmony instead of simply being ignored. Task Scheduler is one of the most underrated parts of Windows, and deserves a better polish, responsiveness, UI, and attention.

]]>
<![CDATA[EF6 to EF Core migration cheatsheet]]>https://ssg.dev/ef6-to-ef-core-migration-cheatsheet/69a68eae17b4d500013d967fTue, 03 Mar 2026 07:41:37 GMT
EF6 to EF Core migration cheatsheet

EF6

EF Core Equivalent

DbModelBuilder

ModelBuilder

DbEntityValidationException

n/a (use Validator.TryValidateObject if you really need to validate)

db.Database.CreateIfNotExists()

db.Database.EnsureCreated()

// composite keys with attributes


[Key]

public int Field1 { get; set; }


[Key]

public int Field2 { get; set; }

modelBuilder.Entity<TEntity>()

  .HasKey(e => new { e.Field1, e.Field2 });

[Index("IX_test", Order = 2]
public int FieldA { get; set; }

[Index("IX_test", Order = 1]
public string FieldB { get; set; }

builder.Entity<TEntity>.HasIndex(e => new { e.FieldB, e.FieldA }).HasName("IX_test");

HasOptional()

HasOne() (nullability determines optionality)

HasRequired()

HasOne(...).IsRequired()

WithOptional()

WithOne() (nullability determines optionality)

WithRequired()

WithOne() (nullability determines optionality)

HasPrecision(x, y)

.HasColumnType("DECIMAL(x,y)");

WillCascadeOnDelete(false);

remove (default behavior)

WillCascadeOnDelete(true);

OnDelete(DeleteBehavior.Cascade);

HasMany().WithMany()

NOT SUPPORTED (use a mapping table to keep the mappings)

SqlFunctions.GetDate()

DateTime.Now (automatically translated in queries, and we don't care about web server/db server time differences in other scenarios as we're keeping them in sync)

SqlFunctions.DateAdd("hour", x, y)

EF.Functions.DateAddHour(x, y)

SqlFunctions.Xxx

EF.Functions.Xxx is your best bet.

SqlFunctions.TruncateTime(dateField)

dateField.Date

SqlFunctions.PatIndex

if it's just "> 0", use EF.Functions.Like(...) instead. 


You can also use "String.StartsWith()" when applicable.

SqlFunctions.DateDiff()

EF.Functions.DateDiffHour()

db.Database.ExecuteSqlCommand("SELECT NULL");

db.Database.ExecuteSqlRaw("SELECT NULL");

db.Database.ExecuteSqlCommand("SELECT * FROM params WHERE field1 = {0} AND field2 = {1}", param1, param2);

db.Database.ExecuteSqlRaw("SELECT * FROM params WHERE field1 = {0} AND field2 = {1}", param1, param2);


--or--


db.Database.ExecuteSqlInterpolated("SELECT * FROM params WHERE field1 = {param1} AND field2 = {param2}");

Database.CommandTimeout = 60;

Database.SetCommandTimeout(60);

db.Database.SqlQuery<int>("SELECT 1 FROM blabla WHERE field1 = {0} AND field2 = {1}", param1, param2);

db.Connection.Query<int>("SELECT 1 FROM blabla WHERE field1 = @param1 AND field2 = @param2", new { param1, param2 });

.GroupBy(e => e.NavPropA)

.Select(g => g.Key.NavPropB.Field1)

.GroupBy(e => e.ForeignKeyFieldA)

.Select(g => db.Table.Where(t => t.FieldA == g.Key).Select(t => t.NavPropB.Field1).Single())


or


.GroupBy(e => e.NavPropA.NavPropB.Field1)

.Select(g => g.Key.Field1)

.Select(e => e.NavPropA.Field1)

.Include(e => e.NavPropA)

.Select(e => e.NavPropA.Field1)

.Where(e => e.Field1.ToString() == EntryStatus.Blabla)


or 


.Where(e => e.Field1 == (string)EntryStatus.Blabla)


.Where(e => e.Field1 == EntryStatus.Blabla);

.GroupBy(...)

.Select(g => g.Select(..).Distinct().Count())

N/A - Only supported on EF Core 5.0+

.GroupBy(...)

.Select(g => g.Select(...).Count())

.GroupBy(...)

.Select(g => g.Count())

.OrderBy(g => g.Max(x => x.Value))

// where x.Value is a non-nullable type

.OrderBy(g => g.Max(x => (type?)x.Value)

]]>
<![CDATA[Click here to see the content]]>https://ssg.dev/click-here-to-see-the-content/6952ed67091c8c00018bbe6eTue, 30 Dec 2025 01:47:03 GMT

We have an ingenious paradigm in user interface design that lets us do away with writing "click here to..." everywhere. It's called a button:

Everyone knows how buttons work on user interfaces. If it looks like a button, that means the "action" in its caption will be performed when it's interacted with. So, this is essentially confusing, unnecessarily verbose, and redundant:

The button styling already tells us that it will perform the action when interacted. But more importantly, "click" isn't a univesal term for UI interaction anymore. We don't click on a touch screen. We "touch" or we "press", but never "click". "Clicking" has never been the only way to interact with a button. We could select a button by focusing on it with keyboard and by pressing space or enter. So, if you prefer to write explicit instructions for your users, you have already failed.

UI is always an abstraction.

Why do we need "click here to..."?

"Click here to..." entered our lives through hyperlinks. Because, many didn't know how to identify a hyperlink and understand that it can be interacted with.

text link text

The problem mostly is that interactive links are visually indistinguishable from blue underlined text. They had no distinct features unlike buttons. It wasn't a problem in the early days of web because styling was usually minimal. But, now with every web page with its own design and colors, it becomes harder and harder to learn what is actually a link, and what is not.

Heck, the same thing happens to buttons. After the whole flat design craze, what's a button anymore? Text with slightly off-colored background?

Is this a button?

That's probably why AmigaOS in the early 90's used button-like design for inline hyperlinks.

Click here to see the content
AmigaGuide-based hypertext documentation in 1992

See how easy it is to distinguish stylized text from hyperlinks? It doesn't even affect the text flow.

I had liked AmigaGuide's approach so much that I had incorporated it in my own hypertext help system I wrote for our DOS GUI back in the 90's.

Click here to see the content
Help screen of my DOS FidoNet mailer app Wolverine v2.30 circa 1997

See, I didn't have to write "click here to read more about registered users" because it was obvious what it was and what it did when clicked.

But, AmigaGuide wasn't a web browser, neither was my help system, and web browsers probably needed to work with fewer colors and many different fonts, possibly with less intrusion. For instance, if you had too many links on a web page, the slab design could make reading hard. Underlines were subtler, and for a time, they were perfectly okay too. Hyperlinks were only blue because some guy liked the color though.

But even underlines were a little bit distracting. Web developers started opting for hyperlinks with distinct colors instead of underlines. They were impossible to discern from other colored text, but once you got the hang of it, you'd learned that what was hyperlink and what wasn't on that specific website.

Because of that people had to emphasize that something was clickable with text: "Click here to..."

The confusing semantics

"Click" isn't an accurate term, but neither is "here". "Here" describes a larger area than the text "here" and less than what the user might consider where "there" is. The UI container could pass as "there" too. So, it usually means "click on the underlined part", if you're lucky.

I find that kind of meta messaging confusing. First, it implies that the user wouldn't know where to click unless specifically instructed. Second, "here" implies a spatial link between the sentence and its coordinates, but there's no such thing. What if someone is using a screen reader? It doesn't make any sense, like how it doesn't make any sense to use other spatial adjectives like "above" or "below" in UI. Don't use those. Let users use their eyes and brains to follow familiar UI layouts.

Sometimes, your UI text doesn't even show up at the places you expect. That's why you usually avoid explicit instructions because they might confuse more than they help. Like how this iPhone notification gives me explicit instructions which don't work at all on my Garmin watch:

Click here to see the content
An iPhone notification showing up on my Garmin watch

Why do you even explain something that's universally applicable to all notifications? Everyone knows tapping the notification navigates to the context of that notification. It doesn't need an explanation. Otherwise it would be like every app having "bug fixes and improvements" in their release notes. And... unforunately, we have that... But, anyway, don't do this. Stop being unnecessarily explicit.

People might have been used to these explicit but confusing instructions. But, that means they've learned their way around the confusion. That means people still had to learn to deal with it. You didn't help people, you just changed what kind of friction they had to deal with. People would have found their way even if you hadn't done anything anyway.

You've just added cognitive overhead of semantically confusing redundancy for everyone.

Let UI do its work

UI is always an abstraction. You can never be explicit enough for everyone. My recommendation is to let users deal with common paradigms, explore, learn, and become effective instead of throwing training wheels under their feet at every opportunity.

  • Use a button that preferably looks like a button with a caption instead of a underlined text with "click/tap/press here to...." whenever there's something clickable that leads to an action or a separate UI page.
  • Avoid spatial references in UI text. You don't know where that UI text will be.
Click here to see the content
Scene from Airplane (1979)
  • If a button isn't available in the context or not suitable, use blue underlined text for hyperlinks as that's now pretty much universal. Don't add "click here to..." before every link.
  • Optimize for people using your app every day, instead of people who's never used a UI before. You're underestimating people's capability to learn.
  • Make exploring your UI inviting. I think Google's "Undo" paradigm is perfect for that.

Make it simple, make it powerful, and trust your users.

]]>
<![CDATA[Use MySQL on WSL2 without a container]]>https://ssg.dev/use-mysql-on-wsl2-without-a-container/690d01fbbbea7f0001c900d4Sat, 29 Nov 2025 04:34:16 GMT

I develop on Windows, but I don't like installing MySQL for Windows because the setup experience is quite terrible. The last time I tried to install an update it asked me to locate an MSI file who knows where for the old installation. So, I finally had it, and installed MySQL on my WSL2 setup.

Why not Docker?

Put it simply, it sounded easier to me to just type sudo apt install mysql on my WSL2 prompt than looking for the right container, right command-line switches at the time.

That was a big mistake.

But, I pushed through and succeeded, and wanted to share my story. Some of the information here might be relevant for other software too, so leaving it here.

There are mainly two problems accessing a MySQL instance in a container over native:

  1. MySQL itself doesn't listen beyond its own localhost by default. So, basically, anyone outside the container becomes an unwanted visitor.
  2. WSL2 instances have their own IP address, not localhost. If your development environment setup uses localhost, you'll have to change all the localhost definitions with your WSL2 IP address which might change in the future too.

Let's solve both problems.

Make MySQL listen to external networks

💡
NOTE: This lowers the security of your MySQL instance. Do not use these settings on a host that is exposed to third parties. It should be safe on a developer machine that uses a firewall. Never use these settings on a server.

Define a password for root

By default, root user on MySQL doesn't have a password, but it's bound to localhost. That prevents it from being accessed remotely. In order to make it accessible from Windows, you need to set a password to it, and change the auth mechanism by using this command in MySQL shell:

ALTER USER 'root'@'localhost' 
IDENTIFIED WITH caching_sha2_password 
BY '<NEW_ROOT_PASSWORD>';

This will change the auth mechanism, and change the root password at the same time. Remember to use a random, strong password, and save it to your favorite password manager. Now, try connecting to your MySQL instance from a WSL2 shell with your new password and make sure that it succeeds.

Change root's host to wildcard

The root user in MySQL has a host of localhost by default. This makes it impossible to login as root from any other host. Run this command in MySQL to change that:

RENAME USER 'root'@'localhost' TO 'root'@'%';

'%' means a wildcard here, and it allows logins to root from any host.

Allow external access to MySQL

Even when you allow external root access, you still can't connect to MySQL container from your Windows machine because MySQL only binds to its localhost, the container instance. You need to make MySQL allow external connections.

Edit /etc/mysql/mysql.conf.d/mysqld.conf and comment out this line:

bind-address            = 127.0.0.1

by prefixing it with "#", so it looks like this:

#bind-address            = 127.0.0.1

Make Windows forward connections to WSL2

This would let any setup for localhost keep working for MySQL.

First, learn your WSL2 IP address with ip addr show eth0 on your WSL2 shell. You need to forward any request to localhost to that address.

Create port forwarding from Windows to WSL2

To do that, run this command on an Administrator Command Prompt on Windows:

netsh interface portproxy add v4tov4 listenport=3306 listenaddress=0.0.0.0 connectport=3306 connectaddress=<YOUR_WSL_IP_ADDRESS>

Test port forwarding

Now, test if you can connect to the MySQL port using localhost by running this on a PowerShell prompt:

Test-NetConnection 127.0.0.1 -p 3306

The output should show TcpTestSucceeded as True:

ComputerName     : 127.0.0.1
RemoteAddress    : 127.0.0.1
RemotePort       : 3306
InterfaceAlias   : Loopback Pseudo-Interface 1
SourceAddress    : 127.0.0.1
TcpTestSucceeded : True

Block external access to MySQL port on Windows

Just to make sure that you don't accidentally expose your MySQL instance to other network peers, you can add rules to let only your localhost connect to your MySQL port for good measure.

Run the commands below on an Administrator command-prompt:

netsh advfirewall firewall add rule name="Allow MySQL from localhost" dir=in action=allow protocol=TCP localport=3306 remoteip=127.0.0.1
netsh advfirewall firewall add rule name="Block MySQL from all hosts" dir=in action=block protocol=TCP localport=3306

The first rule you add will take precedence over the second one.

Et voila!

Now you finally have this weird setup because you didn't care to use a container. Congratulations.

]]>
<![CDATA[Isn't WSL2 just a VM?]]>https://ssg.dev/isnt-wsl2-just-a-vm/68ebed09a466e400012cc5d7Mon, 24 Nov 2025 20:47:13 GMT

"Subsystem" on Windows NT has a vague definition. It basically means an API set, but predominantly for supporting programs written for other operating systems mostly through lightweight call translations. Windows NT, formerly "NT OS/2", used to have an OS/2 subsystem that supported running OS/2 applications just by translating API calls to NT.

Subsystems usually have a separate process for state bookkeeping. There used to be an OS/2 subsystem (OS2SS.EXE), but there were more. Even Windows is a subsystem on NT architecture. The enigmatic CSRSS.EXE is the Win32 API translation layer for Windows NT. You can see the pattern: the "SS" stands for "subsystem". Because of its performance problems, some portions of CSRSS were ported to kernel mode and were called WIN32K.SYS.

There was also a long-lived, pretty much useless POSIX subsystem (PSXSS.EXE) on NT. It was mostly to sell NT to the government because they required operating systems to be at least POSIX.1 certified. It implemented the bare minimum POSIX API to get the certification, and nothing else.

POSIX subsystem was later replaced by a more complete Windows Services for Unix developed by Interix, based on OpenBSD API. Unlike other subsystems, it was not binary compatible with any Unix, but provided its own API set and tooling, so apps could be compiled for it.

Enter WSL1

WSL1 is the first incarnation of Windows Subsystem for Linux. I think the name is silly though because we all know that the actual Windows subsystem for Linux is Wine. I wish Microsoft had kept its initial name: LXSS. Anyway, WSL1 is a thin translation layer like the subsystems I mentioned. When you run bash on WSL1, it only allocates a few MBs of memory just what bash needs, and that's it. You could see and manage bash process in Task Manager along with other Windows processes:

Isn't WSL2 just a VM?
Linux bash showing up next to ASUS bloatware on Windows 11

This is the full process list that shows up on top:

Isn't WSL2 just a VM?

Okay. bash allocates only how much it would allocate on a Linux machine, but isn't there still a runtime cost somewhere? We see at least init here in the list. When we open up the process list in Sysinternals Process Explorer, it shows up in a process tree:

Isn't WSL2 just a VM?

init is there but what else? When I search WSL in Task Manager a few services show up:

Isn't WSL2 just a VM?

Is that it? If I'm not missing anything, that's the overhead of having Linux on your system using WSL1. Only a few megabytes.

The root filesystem of WSL1 also resides on your NTFS partition. Every file exists on the disk separately. This means that the storage overhead could be minimal because adding new files wouldn't require to expand an image file.

Isn't WSL2 just a VM?
WSL1 rootfs on my Windows disk

Downsides of WSL1

WSL1 was a technical marvel, but it suffered serious performance problems on certain I/O heavy scenarios because Linux and Win32 filesystem APIs were too different, and translating them made some apps and some workflows seriously suffer.

There were other limitations with WSL1 too. It required you to install a third-party X Server on Windows to run GUI apps. I myself had used X410 for that purpose. WSL1 also lacked support for raw socket operations because Windows API had restricted them after the Blaster worm incident. So, even traceroute wouldn't run, let alone other raw packet tools like nmap or tcpdump.

Enter WSL2

People rightfully complained because they wanted to use Windows for workflows that needed similar performance characteristics as a native Linux system. So, Microsoft threw the towel and ended up creating a full-blown Linux VM on top of Hyper-V.

Isn't WSL2 just a VM?
"It's an older meme, sir, but it checks out"

Because it's a VM and it works with a native Linux filesystem, the whole root filesystem of Linux stays in a single VHDX file. One of the great things about WSL is that you can convert your installations between WSL1 and WSL2 back and forth with a command like:

wsl --set-version "Ubuntu" 2

Conversion might take a while though as all your files from your root FS are moved from NTFS to a VHDX file or vice versa. Here's my installation after the conversion to WSL2:

Isn't WSL2 just a VM?

You can also have multiple Ubuntu setups, one with WSL1 one with WSL2 and use them interchangeably depending on your needs. Both are supported with different tradeoffs.

WSL2 is faster in certain workflows but it's slower to start initially because of the whole booting up the VM thing. As a reward, you're running a real Linux kernel now:

$ uname -a
Linux SSGHOME3 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

That kernel is provided by Microsoft with many options tuned for WSL2 environment. You can compile your own kernel and run it with WSL2 too if you want. I had to do that once when I needed to access an SD card from my WSL2 setup. They were disabled on default WSL2 kernel at the time.

WSL2 starts up fast enough though, like it takes maybe a second, but still a difference you experience after every reboot. After you start your initial WSL2 session, you now have a perpetually running VM on your system. Luckily though, it doesn't consume as much as memory when idle. This is what it looks like with a single bash prompt:

Isn't WSL2 just a VM?

1.6GB on a 32GB system. Does that mean we only have a 1.6GB RAM VM? No. WSL is smart about this. It claims to allow half my physical memory, 16GB:

$ cat /proc/meminfo
MemTotal:       16324732 kB
MemFree:        15077248 kB
MemAvailable:   15547632 kB

So, I expect the memory usage of VmmemWSL process to grow when you run Linux apps with greater memory requirements. I assume that it probably shrinks back when it needs less memory. So, it's quite good. It's not even enough to malloc the memory, the processes need to commit to it. Otherwise, memory usage doesn't grow at all. Here is a test using the tool stress:

$ stress --vm 16 --vm-bytes 1000000000
stress: info: [1793] dispatching hogs: 0 cpu, 0 io, 16 vm, 0 hdd

That immediately maxes out the memory usage of WSL2, and eventually crashes because available memory is less than 16GB (more like 15GB):

Isn't WSL2 just a VM?

When you kill the app it shrinks back to its original footprint in 10 seconds or so:

Isn't WSL2 just a VM?

I think that's pretty much okay. What'a a gigabyte to spare in these days anyway? Just assume that you're running your clock app under Electron. If you really need the full memory to yourself, you can always shutdown WSL2 from a Windows Command Prompt, and it will start automatically the next time you need it:

  wsl --shutdown

Not too bad. I think the RAM overhead of WSL2 is okay based on the benefits it provides.

Is a VM really an NT subsystem?

First, Microsoft calls WSL2 a subsystem, so I guess it must be. But, based on our definition around API translation layers for ABI imitation, it isn't a subsystem. It's a VM.

32-bit Windows versions used to have an NTVDM (NT Virtual DOS Machine) layer to run DOS and 16-bit Windows applications. It basically worked pretty much similar to WSL2. It wasn't called a "VDMSS.EXE" or so at the time because it didn't fit the definition of a subsystem, but it acted more like a virtual machine, more like WSL2.

However, WSL2 isn't a stock VM either. As I mentioned before, it dynamically allocates memory and shrinks it as needed, it mounts your Windows drives, and lets you access your Linux drives as network shares using \\wsl$\ path syntax, it integrates almost seamlessly with desktop using a layer called WSLg. I say "almost" because every Linux GUI app we're running is actually a Remote Desktop client streaming that app from Linux VM to our Windows machines. It's still great though, apart from not being aware of my desktop settings like text scaling, HiDPI, and whatever. You need to set those up from scratch on your Linux environment.

Downsides of WSL2

My biggest issue with WSL2 is file management. Naturally, WSL2 feels at home when you do all your work on its ext4 image, but your work stays in the image then. An approximately 16GB single file called ext4.vhdx hidden deep inside your hard drive. I don't even know what happens if you uninstall Ubuntu or decide to switch to a different distro. Do you move your files manually between two distros using scp or something like that? I don't like that kind of fragility about keeping your work inside a big disk image.

Okay, I checked it now, and if you run wsl --uninstall Distro your image remains, but if you run wsl --unregister Distro all your files, all that you've worked on are gone in a second. Those two commands are dangerously and Levenshteinly close to each other.

You can use your Windows drives for your work of course. They're automatically mounted anyway, your drive C: is at /mnt/c for example. But, their performance is even worse than WSL1 on NTFS because now both VM overhead and syscall overhead take effect together.

I had been converting my WSL setup between WSL1 and WSL2 while writing this article. I noticed that WSL1 was using drvfs while WSL2 was using 9p protocol, which is Plan9's remote file system protocol. I find the idea of a piece of Plan9 from Bell Labs running on my Windows box quite fantastic to be honest.

I think I found a bug with the conversion process though. When I converted WSL1 distro back to WSL2 again, drvfs mount entries stayed as is, and were not replaced with their 9p counterparts. I got stuck with drvfs, I didn't want to bother with fixing it and just deleted and reinstalled Ubuntu. It's that easy. That's why it would make me uneasy to keep my work in a disk image.

All in all, unless I always remain synced with a remote repository or have backups in place, I'd refrain from doing serious work on an WSL2 root partition. A good middle ground is to use a separate disk image for your work. You can create VHDX images directly from Windows settings:

Isn't WSL2 just a VM?

And you can mount it with wsl --mount --vhd. I strongly recommend that for any serious work. Unfortunately, you can't specify VHDX mounting options in .wslconfig, so you'll have to create wrapper scripts to launch WSL. I wish it were easier.

Conclusion

I might be biased as a former Windows engineer, but I love Windows NT's modular design. I love that I can load a binary-only device driver from 15 years ago on my system and use a legacy device easily on a modern system; a stable kernel ABI goes a long way (hence the saying "Win32 is the only stable ABI for Linux"). Having OS subsystems incorporated into the design from day one is just a cherry on top.

WSL1 has great advantages like extremely lightweight memory overhead if you can bear its limitations. But, WSL2 is no slouch either. It can dynamically grow and shrink back to a gigabyte if you have that to reserve. Or, you can always shut it down if you need more memory.

WSL2 spends good amount of effort to work around the problems of a VM such as heavy memory footprint or lack of integrations with the host OS. I think it would be fair to call it a subsystem, just to separate it from heavyweight VMs.

]]>
<![CDATA[Are cellular towers the next landlines?]]>https://ssg.dev/are-cellular-towers-the-next-landlines/691e34ef86c2660001097953Thu, 20 Nov 2025 23:19:20 GMT
ℹ️
I'm not a telecommunications expert. My latest gig in telecommunications was to design and implement data transfer protocols for Turkish state's meteorological service 30 years ago. These are mostly my ramblings as a consumer.
Are cellular towers the next landlines?

It's strange to me that the carriers are foregoing the long term benefits by acting only for the short term. Take phone calls for example: after months of getting spam calls, I now have all my calls on silent for numbers that aren't in my contacts.

Are cellular towers the next landlines?
This setting has the most peace/price ratio for iPhone.

I understand that fighting spam isn't straightforward, it probably needs regulatory support at the same time, but I have an inkling that it makes carriers substantial amount of money too. That might be why we don't see cellular carriers lobbying for more regulation for spam, if regulation is what we're missing.

I can't even properly identify a number that isn't in my contacts list. We've had "Caller ID" for how many years now, how many decades? There's just no reliable way. Apple sometimes says "Maybe Spam", thankfully. Oftentimes, I just see "Louisville, KY", "Cedar Falls, IA", or another random city and state combination.

I don't call people using my phone anymore unless that's the only way to contact them. The voice quality is lackluster, the international call fees are exorbitant. I use a third-party app like Signal, Whatsapp, or whatever.

I buy a temporary cheap eSIM instead of using my own carrier whenever I travel abroad now because the roaming costs are astronomical. Really, cheap eSIMs themselves prove that how expensive roaming is priced. AT&T charges $12/day for international roaming while an eSIM (data-only) in a foreign country with 50GB limit costs at most $25/month. That kind of gap is just inexplicable to me.

Data-only isn't a hindrance anymore anyway. It's just another push to make me use third party apps instead of phone calls and texting. Cellular carriers are just abandoning their own technological holds with their pricing model. Why does WiFi calling cost extra when you're abroad? The connectivity cost is paid by whoever provides the Internet through ISP infrastructures, not cellular operators. Why do cellular operators charge you for it?

Cellular operators are losing their edge over a cheap data-only eSIM + Whatsapp.

That sounds like the opposite of the path that benefits all parties the most according to the game theory. It feels more like all parties racing to the bottom.

Are cellular towers the next landlines?
Remember the game theory thread?

Because carriers have been too hesitant to let go of their revenue from SMS (I still remember "limited number of messages", "pay per message", "credit top-ups", and so forth), their pushback against higher messaging standards like RCS, we're now used to use Signal, Whatsapp, or a similar app to message people. The only reply we use to an SMS is "STOP" now.

The whole carrier pricing is based on a profit model that doesn't make sense to the consumer. I can call my cellular provider today, tell them I'm cancelling, and they'll immediately make a 20% discount to my plan. That means competition relies on inertia of their customers instead of the added value or competitive pricing. I thought free market would boost innovation. What happened to it?

Cellular operators are slowly becoming expensive network outlets that just transfer packets over the air while they could have been setting an example defining how they improve everyone's lives. Instead, they stagnated, and they are now stale.

RCS was the bare minimum improvement over SMS. It was first launched in 2007, but US carriers could only come to an agreement to use an improved messaging standard in 2019. Apple implemented RCS on iOS 18 which was the version before iOS 26, mind you.

How long until cellular carriers become obsolete?

How long until base stations are replaced with satellites?

Are cellular towers the next landlines?
Consider again that dot. That's here. That's home. That's us.

Satellites are great for outdoor communication, as Apple, Google, and Samsung have already integrated it for emergency messaging for where there is no coverage. I think what that says is that the picture is getting inverted. We'll have coverage everywhere outdoors thanks to satellites, and our houses will be the only place we lack coverage. And for that last mile, people will come up with a solution like WiFi calling.

Why will we need cellular operators then? We all know that base stations are quite fragile. In a disaster, they stop working. That's mostly because they're smaller, more energy efficient devices with only limited capacity while each Starlink satellite can serve 1Tbps. It's also quite straightforward to upgrade satellite based networks: there are no replacement costs because they constantly have to be replaced anyway.

Satellites aside, there are ongoing efforts to create mesh networks independent of cellular towers like Meshtastic. They are obviously nowhere near to cellular or satellite networks in terms of throughput, but it's a definitive sign that people desire alternative ways of connectivity because the mainstream services are unfairly priced.

This happened with cable TV monopolies. They tried to sell you bundles of 200 ad-ridden TV channels, and lousy Internet speeds. Then, Netflix came and freed people from the hegemony of cable. Now, cable providers are trying to sell streaming packages with lousy Internet speeds while local Internet providers like Sonic can give you unlimited 10gigabit Internet for $49.99. See the inexplicable gap?

ℹ️
I'm not affiliated with Sonic, or any other brand, and this isn't an ad. I'm not even a Sonic customer because they won't bother with some conduit permission to my home. It just happened to be an affordable ISP nearby.

I see a similar fossilization happening with cellular providers. They once revolutionized accessibility and telecommunications. But now, their business model feels exploitative and their innovation cycle seems to have stopped.

"All this has happened before, and all this will happen again" --Starbuck

Think about the future we could have if cellular providers had championed improving lives. Roaming and international call fees would be affordable, or perhaps non-existent. Wifi calling would be free. Spam would be managable like how it's with email. Messaging would be functional and easy to use like all other apps. Cellular communications would feel safe, frustration-free, and indispensable.

Instead, phones are becoming modems, and cellular towers are becoming landlines.

Maybe that's inevitable. Maybe that's what's supposed to happen. Maybe cellular operators do want to turn into ISPs because that business is more lucrative. But, if their wish comes true, they'll find themselves competing with 10gigabit for $49.99, and scrambling to bundle Netflix with their outdated infrastructure.

]]>
<![CDATA[Vibe coding in the 90's]]>Hey folks. You know how programming is hard in 1994. You need books, help files, manuals, references to learn about a certain feature. You read all those. Then you go through endless iteration of trial and errors to find out how to accomplish a certain task.

Well, I found a

]]>
https://ssg.dev/vibe-coding-in-the-90s/68fbeb77aa32560001a7ff4cFri, 24 Oct 2025 23:48:12 GMT

Hey folks. You know how programming is hard in 1994. You need books, help files, manuals, references to learn about a certain feature. You read all those. Then you go through endless iteration of trial and errors to find out how to accomplish a certain task.

Well, I found a revolutionary way to write code. You see, I got this CD-ROM from a friend. It's full of public domain sample code for every kind of application you could write.

You want to write a file manager like Norton Commander? Well, just browse to the fileman\ofm\nc folder in the CD. Copy all the files to your local disk. And you're done! You have a full-blown, working file manager code.

There are thousands of ready to run projects like these. All you need to do is to imagine what you want to create, and it's just a few clicks to get the project you want. CAD, drawing, accounting, games, you name it, and your project is ready in seconds.

You can even use individual components under lib folder and include them to implement new features. There are libraries for everything from parsing GIF files to playing MIDI on Sound Blaster Pro. Everything you can imagine.

That's very exciting! I think we're reaching to the end of programming because as the number of these libraries and projects grow, our need to write code will eventually disappear. All we need to do is to find the template, put needed libraries on top of it. That's going to be it. We won't be needing programmers anymore.

I mean, maintenance is still a chore, but the big part of the work will already be done when you start. So, it's only the small part of adding several lines here and there and that's it.

Obviously, to change something in the project, we first need to understand how it works and how it's designed, but that's easy too. You see there's a file in the root of every project called PROMPT.TXT that explains the project from a 10,000 feet. Then there's SPEC.TXT that we can read to understand how everything is architectured. There is also an INDEX.TXT that contains all the list of files and why they exist. After that you can go through API.TXT and WHATSNEW.TXT to entirely understand how everything fits in place. Then, all there's left is to read some of the code files to understand how it's written. As long as you know the programming language and the used techniques in the project, you should have no trouble doing that either.

Even a child can code now. You just read all these prompts, specifications, file lists, and APIs, changelogs, and completely grasp the architecture of the project that a random person wrote. When you're done with this, it's now your project, and you can go ahead and maintain it easily.

Anything more complex than a few lines, you can just copy it from lib\ folder of the CD-ROM. There's a component for everything. You want to left-pad a string? Then, go to the folder lib\str\basic\pad\left and pick one of the hundreds of libraries doing the same job. Pick a random one if you will. Most of them work fine.

If you encounter any bugs in a library, replace it with another. Or just read the prompt, read the specs, read the file listing, understand the whole architecture, grasp the architecture, and finally fix it. It's a breeze working this way. I feel like I regained joy of creating software again.

I'm actually making stuff now. Look, I just wrote Doom yesterday in minutes! It's just a matter of copying the folder games\3d\doom and pressing compile and Run! I have written a 3D game from scratch! How cool is this? The possibilities are endless!

I feel like I can create anything.

Now, what shall I create next? Anything. I can create anything. Let me just take a look at that CD. It had some good ideas in there.

The future is here, folks!

]]>
<![CDATA[From passwords to passkeys]]>https://ssg.dev/from-passwords-to-passkeys/68c7a2dd0124be0001a04a7eThu, 09 Oct 2025 00:12:49 GMT
  • 1961: A group of researchers at MIT releases CTSS, the first password-based, multi-user time-sharing system due to long queues in front of the only keyboard in the entire department.
  • 1961: password used as password for the first time.
  • 1961: First account hacked.
  • 1967: Barclays Bank in UK introduces the first ATM (Automated Teller Machine). Initially slated to use 6-digit PINs, but ends up with 4-digit PINs because 6-digits were considered too hard to remember[1].
  • 1968: Roger Needham theorizes one-way encryption that will eventually become what's known as "hashing". It becomes a breakthrough that has led us to inventions like BitTorrent and monkey NFTs.
  • 1969: The first Man in the Middle is born.
  • 1974: Robert Morris introduces salting to hashes to store passwords to make hacking slightly more annoying.
  • 1978: Alice and Bob born to separate families in rural Atlanta.
  • 1982: First time, someone discovers the best password, and decides to use it everywhere.
  • 1983: The movie WarGames released, warning the public about the dangers of weak passwords and chess.
  • 1985: Sun Microsystems invents "password shadowing" that moves password hash data that was stored in /etc/passwd to another file, aptly named shadow, in the same directory, so hackers will never find it. Morgan Freeman would later calmly state that hackers, in fact, have found it.
  • 1985: Zero-knowledge proofs theorized by a group of unknown people.
  • 1989: Tim Berners-Lee invents the World Wide Web. This has made a lot of people very angry, and been widely regarded as a bad move[2].
  • 1990: Ron Rivest (the "R" of RSA) releases the hashing algorithm MD4.
  • 1991: Gillette releases MD5.
  • 1992: First SMS sent.
  • 1992: First person left on read.
  • 1993: Ari Luotonen at CERN invents HTTP Basic authentication for Web that prevents username and password from being snooped on the network by encoding them in Base64. It relied on the assumption that base64 tool to decode them would take forever to download from Simtel mirrors.
  • 1994: The web browser Netscape Navigator introduces encrypted HTTPS (HTTP over SSL) protocol that shows a warning when used, so people can make an informed decision to be secure or not by doing their own research.
  • 1997: The small banner that says "this website is secure" is invented. People start hanging it on their web page to make it secure, and for good luck.
  • 1998: The first time "1!" added to the end of a password to make it secure.
  • 2001: First password requirement of having at least one uppercase letter invented. Suddenly, the popularity of Password surges.
  • 2002: Bruce Schneier releases "Password Safe", the first ever password manager. People see no point in storing their only password in a tool.
  • 2003: Banks start adopting SMS as a secondary factor for authentication. Hacking becomes a viable career path for a broader audience again.
  • 2004: User AzureDiamond boldly types their password hunter2 in an IRC channel only to be surprised to see that it hasn't appeared in all asterisks despite the assurances of the other users.
  • 2004: OATH, The Initiative for Open Authentication, founded as an industry-wide collaboration to eliminate passwords completely.
  • 2006: 1Password gets founded and instantly becomes the top password manager in alphabetical order.
  • 2006: OpenID Foundation founded to eliminate passwords completely.
  • 2007: YubiKey founded to eliminate passwords profitably.
  • 2008: MySpace hacked. Hackers find everything but users.
  • 2008: IETF, Internet Engineering Task Force, accepts OATH's proposal for TOTP (Time-based One Time Password) also known as "constantly changing six digit numbers" which would force the scammers to change their workflow to ask for that code too, adding 2 minutes per scam.
  • 2008: MD5 cracked accidentally by a toddler.
  • 2010: Google launches Authenticator app to store TOTP 2FA codes in hopes of killing it in five years.
  • 2011: Valve's founder Gabe Newell publicly discloses his Steam account password as mollyftw at a public event.
  • 2011: Gabe Newell publicly confirms that Steam forums were hacked.
  • 2013: FIDO Alliance founded to eliminate passwords completely.
  • 2013: Security researcher Troy Hunt releases the online service "Have I Been Pwned?" that checks if your password is in a leaked data set or not, and if not, adds it to the list.
  • 2014: FIDO Alliance releases U2F (Universal Second Factor) specification that does not eliminate passwords whatsoever.
  • 2017: NIST, National Institute of Standards and Technology, releases Digital Identity Guidelines which suggests that passwords may not actually become more secure when users increase the number at the end every three months.
  • 2018: FIDO Alliance and W3C (World Wide Web Consortium) join forces to eliminate passwords completely.
  • 2019: The FIDO2 standard gets released. It encompasses technologies like WebAuthn, CTAP, and uses secure public-key cryptography to allow user authentication without using passwords. Since nobody understands that, they propose the term "passkey" instead.
  • 2020: Microsoft announces that they intend to make all accounts passwordless. Xbox users typing their password using a Ouija board rejoice.
  • 2021: Apple announces their strong support for passkeys, and files a patent immediately for a proprietary alternative that's fully incompatible with Android.
  • 2022: First time in history, a WiFi access point gets a strong password but only until Christmas Eve.
  • 2023: 1Password releases passkeys.directory website to showcase web sites supporting passkeys, and streamline the adoption.
  • 2023: Hackers release passkey.directory website to streamline phishing.
  • 2024: Amazon adopts passkeys but decides to keep passwords too as Jeff Bezos doesn't want to abruptly alienate scammers.
  • 2024: Person implodes while trying to recover their passkey-secured email account.
  • 2025: Troy Hunt, the creator of "Have I Been Pwned?", gets pwned by a phishing email.
  • 2025: Still nobody knows what a passkey is.
  • ]]>
    <![CDATA[Safe zero-copy operations in C#]]>https://ssg.dev/safe-zero-copy-operations-in-c/68d4858b1dd942000145caa8Mon, 29 Sep 2025 23:09:52 GMT

    C# is a versatile language. You can write mobile apps, desktop apps, games, websites, services and APIs with it. You can write it like Java with all the abstractions and AbstractionFactoryClassProviders. But differently from Java, you can write low-level and unsafe code too. When I say low-level, I mean without the GC, with raw pointers.

    Low-level code is usually required for performance or interoperability with C libraries or the operating system. The reason low-level code helps with performance is that it can be used to eliminate runtime checks on memory accesses.

    Array element accesses are bounds-checked in C# for safety. But, that means that there's performance impact unless the compiler can eliminate a bounds-checking operation. The bounds-checking elimination logic needs to ensure that the array index was already bounds-checked before, or can be assured to be inside bounds during the compile-time. For example, take this simple function:

    int sum(int[] array)
    {
      int sum = 0;
      for (int i = 0; i < array.Length; i++)
      {
        sum += array[i];
      }
      return sum;
    }

    That's an ideal situation for bounds-checking elimination because the index variable i is created with known boundaries, and it depends on the array's length. The index variable's lifetime is shorter than the array's lifetime and it's guaranteed to be contained valid values throughout the function. The native code produced for sum has no bounds-checking:

    L0000	xor	    eax, eax
    L0002	xor	    edx, edx
    L0004	mov     r8d, [rcx+8]          ; read length
    L0008	test    r8d, r8d              ; is empty?
    L000b	jle	    short L001c           ; skip the loop
    L000d	mov	    r10d, edx
    L0010	add	    eax, [rcx+r10*4+0x10] ; sum += array[i];
    L0015	inc	    edx                   ; i++
    L0017	cmp	    r8d, edx              ; compare length with i
    L001a	jg	    short L000d           ; loop if still greater
    L001c	ret	

    But, what if the function signature was slightly different?

    int sum(int[] array, int startIndex, int endIndex)
    {
      int sum = 0;
      for (int i = startIndex; i <= endIndex; i++)
      {
        sum += array[i];
      }
      return sum;
    }

    Now, the C# compiler doesn't have a way to know if the passed startIndex and endIndex values are inside the boundaries of array because their lifetimes are distinct. So the native assembly produced becomes way more involved with bounds-checking operations:

    L0000	sub		rsp, 0x28				
    L0004	xor		eax, eax			; sum = 0
    L0006	cmp		edx, r8d			; startIndex > endIndex?
    L0009	jg		short L0045			; then skip the entire function
    L000b	test	rcx, rcx			; array is null?
    L000e	je		short L0031			; then cause NullReferenceException
    L0010	mov		r10d, edx
    L0013	or		r10d, r8d
    L0016	jl		short L0031
    L0018	cmp		[rcx+8], r8d		; array.Length <= endIndex ?
    L001c	jle		short L0031			; then do bounds-checking
    L001e	xchg	ax, ax				; alignment NOP
    L0020	mov		r10d, edx
    L0023	add		eax, [rcx+r10*4+0x10]   ; sum += array[i]
    L0028	inc		edx					; consider i + 1
    L002a	cmp		edx, r8d			; i > endIndex?
    L002d	jle		short L0020			; no, go on
    L002f	jmp		short L0045			; return
    L0031	cmp		edx, [rcx+8]		; i > array.Length?
    L0034	jae		short L004a			; bounds-checking failed. go to ----+
    L0036	mov		r10d, edx												|
    L0039	add		eax, [rcx+r10*4+0x10]	; sum += array[i]				|
    L003e	inc		edx					; i++								|
    L0040	cmp		edx, r8d			; i <= endIndex ?					|
    L0043	jle		short L0031			; continue for loop					|
    L0045	add		rsp, 0x28												|
    L0049	ret							; return sum						|
    L004a	call	0x00007ff857ec6200	; throw IndexOutOfRangeException <--+
    

    We could use low-level unsafe functions and pointers in C# (yes, C# supports raw pointers!) to avoid bounds-checking altogether, like this:

    unsafe int sum(int* ptr, int length)
    {
      int* end = ptr + length;
      int sum = 0;
      for (; ptr < end; ptr++)
      {
        sum += *ptr;
      }
      return sum;
    }

    That also creates very optimized code that supports passing along a sub-portion of an array:

    L0000	movsxd	rax, edx        
    L0003	lea	rax, [rcx+rax*4]    ; end = ptr + length
    L0007	xor	edx, edx            ; sum = 0
    L0009	cmp	rcx, rax            ; ptr >= end ?
    L000c	jae	short L0019         ; then return
    L000e	add	edx, [rcx]          ; sum += *ptr
    L0010	add	rcx, 4              ; ptr += sizeof(int)
    L0014	cmp	rcx, rax            ; ptr < end?
    L0017	jb	short L000e         ; then keep looping
    L0019	mov	eax, edx
    L001b	ret	                    ; return sum

    Unsafe code and pointer-arithmetic can be very performant as you can see. The problem is that it's too dangerous. With incorrect values of length, you don't simply get an IndexOutOfRangeException, but instead, your app either crashes, or returns incorrect results. If your code happened to modify the memory region instead of just reading it, then you could have a nice entry point for a buffer overflow security vulnerability in your app too. Not to mention that all the callers of that function will have to have unsafe blocks too.

    But it's possible to handle this safe and fast in C# without resorting to esoteric rituals like that. First, how do you solve this problem of indexes to describe a portion of an array and actual boundaries of the array being disconnected from each other? You create a new immutable type that holds these values together. And that type is called a span in C#. Other programming languages may call it a slice. Declaration of Span type resembles something like this. Well, it's not exactly this, but I want you to understand the concept first:

    readonly struct Span<T>
    {
      readonly T* _ptr;
      readonly int _len;
    }

    It's basically an immutable pointer with length. The great thing about a type like this is that the compiler can assure that once an immutable Span is initialized with correct bounds, it will always be safe to access without any bounds-checking. That means, you can pass around sub-views of arrays or even other spans safely and quickly without the performance overhead.

    But, how can it be safe? What if the GC decides to throw away the structure that ptr points to? Well, that's where "ref types" come into play in C#.

    A ref type is a type that can't leave the stack and escape to the heap, so it's always guaranteed that a T instance will outlive a Span<T> instance derived from it. That's why the actual Span<T> declaration looks like this:

    readonly ref struct Span<T>  // notice "ref" 
    {
      readonly ref T _ptr;        // notice "ref"
      readonly int _len;
    }

    Since a ref type can only live in stack, it can't be a member of a class, nor can it be assigned to a non-ref variable, like, it can't be boxed either. A ref type can only be contained inside another ref type. It's ref types all the way down.

    Span-based version of our sum function can eliminate bounds-checking, and it can have other super powers too. The first one is that it can receive a sub-view of an array:

    int sum(Span<int> span)
    {
      int sum = 0;
      for (int i = 0; i < span.Length; i++)
      {
        sum += span[i];
      }
      return sum;
    }

    For instance, you can call this function with sum(array) or you can call it with a sub-view of an array like sum(array[startIndex..endIndex]). That wouldn't incur new bounds-checking operations other than when you're trying to slice the array using the range operator. See how the generated assembly for sum becomes optimized again:

    L0000	mov	rax, [rcx]
    L0003	mov	ecx, [rcx+8]
    L0006	xor	edx, edx            ; sum = 0
    L0008	xor	r8d, r8d            ; i = 0
    L000b	test	ecx, ecx		; span.Length == 0?
    L000d	jle	short L001e
    L000f	mov	r10d, r8d
    L0012	add	edx, [rax+r10*4]    ; sum += span[i]
    L0016	inc	r8d                 ; i++
    L0019	cmp	r8d, ecx            ; i < Length?
    L001c	jl	short L000f         ; then keep looping
    L001e	mov	eax, edx            
    L0020	ret						; return sum

    Another super power you get is the ability to declare the data structure that you receive as "immutable" in your function signature, so that function is forbidden from changing it, and you can find relevant bugs instantly. All you need to do is to replace Span<T> with ReadOnlySpan<T>. Then your attempts to modify the span contents will immediately cause a compiler error. Something impossible with regular arrays, even if you declare them readonly. The readonly directive only protects the reference from modification not the contents of the data structure.

    Passing along a smaller portion of a larger data structure to relevant APIs used to involve either copying or passing the relevant part's offset and length values along with the data structure. It required the API to have overloads with ranges. It was impossible to guarantee the safety of such APIs as the relationship between parameters couldn't be established by the compiler or the runtime.

    It's now both easy and expressive to implement zero-copy operations safely using spans. Consider a Quicksort implementation for instance; it usually has a function like this that works with portions of a given array:

    int partition(int[] array, int low, int high)
    {
      int midpoint = (high + low) / 2; // I know, we'll get there!
      int mid = array[midpoint];
    
      // tuple swaps in C#! ("..^1" means "Length - 1")
      (array[midpoint], array[^1]) = (array[^1], array[midpoint]);
      int pivotIndex = 0;
      for (int i = low; i < high - 1; i++)
      {
         if (array[i] < mid)
         {
           (array[i], array[pivotIndex]) = (array[pivotIndex], array[i]);
           pivotIndex += 1;
         }
      }
      (array[midpoint], array[^1]) = (array[^1], array[midpoint]);
      return pivotIndex;
    }

    This function receives an array, start and end offsets into the array, and rearranges the items based on a picked value in it. Values smaller than the picked value move to the left, larger values move to the right.

    The mid = array[midpoint] has to be bounds-checked because the compiler can't know if the index is inside the bounds of this array. The for loop also performs bounds-checking for array accesses in the loop, which some of them can be eliminated, but not fully guaranteed.

    There is also an overflow error because we pass array ranges using high and low values: (high+low) can overflow for very large arrays, and the results would be catastrophic, can even cause buffer overflow exceptions.

    The partition function gets recursively called many times by Quicksort function below. That means bounds-checking can be a performance issue.

    void Quicksort(int[] array, int low, int high)
    {
      if (array.Length <= 1)
      {
        return;
      }
    
      int pivot = partition(array, low, high);
      Quicksort(span, low, pivot - 1);
      Quicksort(span, pivot + 1, high);
    }

    With spans, the same Quicksort function looks like this:

    void Quicksort(Span<int> span)
    {
      if (span.Length <= 1)
      {
        return;
      }
    
      int pivot = partition(span);
      Quicksort(span[..pivot]);
      Quicksort(span[(pivot + 1)..]);
    }

    See how expressive using spans are especially with the range syntax? It lets you get a new span out of an existing span or an array using double dots (..). Even the partition function looks much better:

    int partition(Span<int> span)
    {
      int midpoint = span.Length / 2; // look ma, no buffer overflows!
      int mid = span[midpoint];
      (span[midpoint], span[^1]) = (span[^1], span[midpoint]);
      int pivotIndex = 0;
      for (int i = 0; i < span.Length - 1; i++)
      {
         if (span[i] < mid)
         {
           (span[i], span[pivotIndex]) = (span[pivotIndex], span[i]);
           pivotIndex += 1;
         }
      }
      (span[midpoint], span[^1]) = (span[^1], span[midpoint]);
      return pivotIndex;
    }

    Because C# spans are also zero-based, it's harder to have buffer overflow problems caused by formulae like (low + high) / 2. Now, the implementation is as fast as an unsafe implementation with raw pointers, but still extremely safe.

    New zero-copy operations in .NET runtime

    I used examples that use recursive calls to show how sub-portions of a larger data structure can be passed to another function without copying, but spans can be used almost everywhere, and now .NET runtime supports zero-copy alternatives of popular functions too.

    Take String.Split for example. You can now split a string without creating new copies of every split portion of the string. You can split a CSV line into its parts like this:

    string csvLine = // .. read CSV line
    string[] parts = csvLine.Split(',');
    
    foreach (string part in parts)
    {
      Console.WriteLine(part);
    }

    The problem with that is, now you're dealing with five new memory blocks with varying lengths. .NET allocates memory for them, GC keeps track of them. It's slow, it hogs memory. It's problematic especially in loops, and can create GC pressure, slowing your app even more.

    You can instead cast your CSV line into a ReadOnlySpan<char> and iterate over its components to write it to the output:

    string csvLine = // .. read CSV line
    
    var span = csvLine.AsSpan();
    var parts = span.Split(',');
    foreach (var range in parts)
    {
      Console.Out.WriteLine(span[range]);
    }

    Note that we use a small detour to use Console.Out.WriteLine instead of Console.WriteLine because Console class lacks an overload to output a ReadOnlySpan<char> like a string.

    The great ROI of that experiment is making zero memory allocations after reading CSV line into memory. A similar logic that improves performance and memory efficiency can be applied everywhere that can receive a Span<T>/ReadOnlySpan<T> instead of an array.

    Embrace the future

    Spans and slice-like structures in are the future of safe memory operations in modern programming languages. Embrace them. The quick takeaways are:

    • Use spans over arrays in your function declarations unless you explicitly require a standalone array in your function for some reason. Such a change opens your API into zero-copy optimization scenarios, and calling code will be more expressive.
    • Don't bother with unsafe/pointers if you can code the same logic with spans. You can still perform low-level memory operations without wandering into the dangerous parts of the forest. Your code will still be fast, and yet safer.

    Use spans, wherever possible, mostly readonly.

    ]]>
    <![CDATA[Don't purchase your iPhone with AppleCare+]]>https://ssg.dev/dont-purchase-your-iphone-with-applecare/68d1ce2e7ac6e900019c29ddMon, 22 Sep 2025 23:19:12 GMT

    I purchased an iPhone 17 Pro w/AppleCare+ online because that sounded like a good idea. I received the phone over mail. I opened the package, set the phone up, and activated my account. A few minutes later, I received a notification from iOS to "Add AppleCare+ coverage". Red dot and everything.

    I thought I didn't purchase my device with AppleCare+, so I went ahead and paid for AppleCare+, and activated it. I didn't want my device to remain uncovered. The activation went successful.

    The next day, I started getting a barrage of emails from Apple with the subject "Problem with AppleCare+ coverage for your device", every half an hour:

    Don't purchase your iPhone with AppleCare+

    These emails were flooding my inbox, and I wasn't even sure if this was related to my current purchase or the last one. The email said there was a billing problem:

    Your purchase of an AppleCare+ with Theft and Loss plan listed above could not be completed because of a problem with your payment method.

    I thought of cancelling AppleCare+ as a solution. This way, if it was the original purchase, it would kick in, if it's not, I could try purchasing it again. Because I had no idea what the problem was. So I went ahead to cancel AppleCare+, but the cancellation screen strongly warned me against that as I could never reactivate AppleCare+ again once I cancelled it. Scary warning. I decided not to do anything.

    So, I called Apple Support. That wasn't straightforward either, I had to navigate through links until I get a "get me a human" link. Luckily, they called me back in two minutes or so after that. After learning about my problem, the support person told me that they had to connect me to an AppleCare+ specialist, and put me on hold.

    I waited on the phone for about 40 minutes probably because it was only days after people got their first batch of iPhone 17's and the support was swamped. The support person apologized for the wait. AppleCare+ specialist greeted me, also apologized for the wait, and explained to me that the email should stop after 3 days, and I would get refunded for my initial AppleCare+ purchase along with iPhone. He told me that it could take up to 15 business days to get my refund.

    Since I already had my email rules in place I said I didn't mind that. For someone else with less email handling skills, that might have been quite frustrating though.

    The specialist also told me that, the emails might not stop after three days, then in that case, they had to manually intervene and deactivate that themselves. So I had to call them again in case the wait for three days formula didn't work.

    Because of that, I set up a reminder for that and waited until Monday. And on Monday, it all went calm. No more emails. I disabled my email rule. Deleted the spam. I guess, I'll get a refund in 15 days or so. I'll edit this when I get it.

    The issue seems to have been resolved right now, but has left a bitter taste in my mouth:

    • Why did Apple fail to activate my originally purchased AppleCare+ when I activated my iPhone with my account that I also used to purchase AppleCare+ plan with?
    • Why did it even recommend me to purchase AppleCare+ while I obviously had it? Why didn't it recommend to activate the existing one?
    • Had I waited for AppleCare+ to kick in the next day instead of purchasing one at the time, would it cover the first day without AppleCare+? How much trouble was I looking at for that? How many support calls would I have to make?
    • Why did Apple email me every half an hour for AppleCare+ coverage issue that didn't affect my AppleCare+ coverage the slightest?
    • Why did they call it a "problem with your payment method" while it was in fact an activation issue with my original purchase? Why couldn't Apple identify and process an issue like this accurately?
    • Why can't I get an immediate refund for an issue like this? Why do I have to bear the cost of Apple's mistake here?

    I love Apple products, especially the iPhone, and use many of them. But, that kind of screwup was very unlike of Apple. I mean, Apple once made my friend's all Apple Wallet passes appear in my Apple Wallet while I was abroad (7000 miles away from him, and dismissed it as a nothingburger, but that's maybe a story for another post. Anyway, that hurt my confidence in Apple's quality of handling the purchase pipeline. I can't imagine an average person who needs access to use their email while handling an issue like this.

    I believe that a three trillion (T as in Tim) dollar company could have done a better job implementing this pipeline. Add necessary checks, implement more informative notices, understand the problems of flooding the customer's inbox, put an immediate refund/cancellation flow in the pipeline in case an issue like that was detected. Achieving all that would cost Apple, I don't know, a hundred grand? It's not like AppleCare was released this year either. It's been around for a long time. I don't want to believe that nobody had thought their purchase/activation pipeline would allow the customer to purchase a redundant AppleCare+.

    The next time, I wouldn't purchase AppleCare+ with my device, but purchase it right after I received it. It seems to be way less hassle than paying for it upfront.

    ]]>
    <![CDATA[Good people doing good things]]>https://ssg.dev/good-people-doing-good-things/6879ffb801f2b1000118ceeaWed, 06 Aug 2025 22:36:19 GMT

    I started writing this as a series of Bluesky posts, but it got longer than I thought.

    I watched Scott Hanselman's TEDx talk titled "Tech Promised Everything. Did it deliver?". It's a beautiful talk. I strongly recommend you to watch it before reading this.

    At some point, Scott shows the viewers a box of a Commodore 64. I just realized that the same one was sitting right behind me while I was watching it:

    Good people doing good things
    My C64 and its box

    The talk made me tear up like Scott. My parents also "sold the van" to buy me a computer when I was only 11. I also had utopic dreams about how tech might change our lives in magical ways. I almost fully agree with Scott's points. But, I believe that the talk is more focused on the pessimistic side, as if there was a good path we could take, but we ended up in a bad path. Scott had to use a very limited time to present a very broad problem, and did a fantastic job. I just want to expand on Scott's points to make the picture clearer.

    Good people are here, and they're doing fantastic things

    Scott shows a screenshot of Richard Stallman's email that announces GNU project in 1983. This is a screenshot from his video:

    Good people doing good things
    with the gorgeous VT100 font too!

    Note that the email says "contributions of time, money, programs and equipment are greatly needed". Today, GNU is a beast. Thousands of developers around the world contribute to it. It kicked off the free software movement, greatly participated in making Linux what it is today, and thanks to it, the blog platform that I'm writing this on, Ghost, is free software. I can self-host my blog anytime if I want. Free software movement has changed so many things for the better. Thanks to it, anyone can write an email to BMW, and get all the GPL source code they wrote.

    BOINC project allowed numerous academical projects to be researched by pooling the CPU power of the computers from all around the world. The project has spawned more than a thousand academical papers. A feat that was unimaginable in the 80's.

    Signal singlehandedly spearheaded the end-to-end encrypted messaging. Thanks to it, even big tech adopted E2E, and increased our privacy. Yes, governments are trying hard to circumvent it, but we're still way better off than we were a decade ago. Similarly, Let's Encrypt movement made web more secure at scale.

    If I start counting free software success stories one by one, this would be a very big article: Ubuntu, Linux, Git, FFmpeg, and OBS to name a few. We have orders of magnitude more good people doing great things in tech than we had in the 80's. Most of the world runs on free software. It's a vision realized.

    Big tech is amazing too

    Yes, Siri can't understand me, yes, autocomplete sucks, yes, ChatGPT lies its ass off, yes, the web is an ad-ridden, unreadable mess, yes, privacy is something we never get without a fight. But, as Scott showed in the talk, we have access to immense computing power, and can do great things with it. That's pretty much fully thanks to big tech companies like Intel, AMD, Apple, ARM, and NVidia. This immense computing power is in our pockets because of big tech.

    Internet is faster than ever and so ubiquituous that groups can communicate in realtime with video. That was a sci-fi dream since the early 20th century and up until the 80's, and was only realized fully and at scale over the last decade by big tech mostly. Boutique ISPs are emerging with even better alternatives like Sonic providing unlimited 10 gigabit internet for only $60 a month, but we need more anti-monopoly work ahead of us in order to unleash those small competitors so they can pressure big players into better prices and faster speeds.

    We have 5G cellular networks with gigabit speeds in the cities. Connected is the norm, the exact opposite of what we had in 2000's.

    Games look mind-blowing. Despite taking their own sweet time, VR/AR are shaping up to be great experiences, and are actually usable in daily lives. We can reach anyone, anytime, even when there is no cellphone coverage, using satellites. I can't overstate how far we've come.

    We can almost "download a car" because we have things called 3D printers! We have self-driving cars, on the road, today! I was a kid when I watched Knight Rider, and the possibility of having such a technology was only a dream to me.

    Good people doing good things
    How about that, Michael?

    So, why do we think that big tech is bad?

    Think about all the things that makes big tech look bad: ads, planned obsolescence, subscription models, paywalls, walled gardens, monopolies, unwanted features, bloat, lack of privacy, too much hype, too little substance.

    There is only one common part among all those: money. The reason we feel bad about tech is because money has finally arrived. Yay, I guess.

    Good people doing good things
    Computer Design magazine, August 1983

    It took its time though. Remember, Amazon had to report losses for a decade after it was founded in 1994. Money wasn't "online" in the 90's. It was curious, checking Internet out, but it wasn't online all-in. Today, it's all-in. Our communication channels have been overwhelmed with money. Money is everywhere... and it's sucking up our air.

    Money bothers us because it forces bad mode of incentives. We need money. We need money to survive. We need money for healthcare. We need money not to become homeless. We constantly need money, therefore, our mode of working must keep us profitable. So, profit maximization becomes a goal of individuals, not just corporations. In that case, you need to be a part of a system that works for profit maximization. You need to integrate with this flawed ecosystem because you may not survive otherwise.

    Money isn't bad as long as we can breathe between its gears. But, when everything becomes about money, fundamental human needs like privacy and social interactions get harmed by it. You follow your friend on your Instagram, and he praises a product. Does he do it sincerely, did he get an ad gig, is that a referral link? It harms interpersonal trust and authentic social connections.

    When only incentive is money, Instagram doesn't stop at being a nice photo sharing app. It strives to become a dopamine faucet. Every app tries to become the most addictive instead of being the most useful.

    The good money

    Not all money is the same. Ad driven money is inevitably focused on getting as much as data from you, and presenting you as many ads as possible. So, it's probably the worst in terms of abusing your privacy and mental health. But, subscriptions aren't as bad. I don't like every Internet corner being paywalled right now, because I don't want to subscribe to something indefinitely just because I liked one article, but I expect them eventually turn into bundles, instead of individual subscribers, like cable boxes we had that came with 120 channels.

    In a similar vein, I think that monthly payments for other types of services are okay too. Those services are incentivized to make you happy, not the advertisers. The most prominent example is Kagi for me. It's a search engine that respects your privacy, provides great search results with many customization options: the best one being the ability to remove Pinterest from search results. It also gives access to many AI assistants for free, and is fully ad-free. Kagi has been aware of bad mode of incentives, and that's why they don't have a referral program, but they occasionally provide free trial codes. I've been using it for two years, and I'd never once had better results shown to me by Google.

    Good people doing good things
    Unlike Google, Kagi shows you AI results ("Qucik Results") only when you end the query with a question mark

    Not all monthly subscriptions are good though. The worst example could be Adobe Photoshop. I understand that their cause to battle with piracy, but, monthly payments for something even when you use it entirely locally? It sounds ridiculous to me. That's why, me not being a professional designer notwithstanding, Paint.NET is my favorite. Although it's free, I intentionally paid for its Microsoft Store app to support its development. It's a fantastic tool for basic image and photo manipulation. I hope to see it on other platforms other than Windows in the near future.

    Good people doing good things
    Paint.NET, the best MS Paint alternative on Windows

    When I was using a Mac, I had paid for fantastic tools like Affinity Designer and Affinity Photo both great Photoshop alternatives without a monthly subscription. Although they were purchased by Canva recently, they seem to be keeping their one-shot payment business model which is what it was supposed to be for a local-only software. I haven't been using them for years, but I know that I can go ahead and install them tomorrow if I wanted to. That's what ownership is about.

    I'm okay with paying for remote services monthly because I know that they have ongoing costs for the provider based on your usage metrics. I'm not okay for anything other than paying for major version upgrades for local-only (or remote-optional software) though. It feels like extortion to me. Make your development costs for bug fixes and small improvements part of your pricing model, like games, I'm perfectly fine with that. I like Jetbrains IDEs more than Micrsofot's Visual Studio for that reason: you can keep using your software indefinitely without your license expiring as with Visual Studio. But, you can keep paying for annual upgrades if you want for improvements and whatever. That's how it should be.

    I paid for Zorin OS for the same reason. I'm not a Linux user, but it's a beautiful distro that's very easy to adapt to for Windows users like me. I wanted to support its development, and having perpetual access to it without worrying about my subscription expiring incentivized me to pay for it.

    Good people doing good things
    Zorin OS can be configured to look very much like Windows

    Berkeley Mono from US Graphics is another success story in that regard. It's a beautiful monospaced typeface. I love it, and the one-time payment for a perpetual license to use it. I'm all paying for beautiful stuff as long as it feels fair. A substantial upgrade from the V1 version required a payment too, but it didn't suddenly invalidate the license you had.

    Good people doing good things
    Hands down the best mono typeface

    You can see that there is a pattern to this. Ad-based free services are almost universally bad for you. Subscription is fine if you're paying for something ongoing. Paying for software for one-time if it's local-oriented use is the best. Commercial software developers should be focusing on these models, and hopefully leave abusive practices like "renew your license to use this software every month that you don't even need Internet for".

    So, deriving from the wise words of Michael Pollan, my advice would be:

    Pay for stuff, not too much, mostly once.

    It's a way healthier approach than trying to live on "free" apps and services, but actually paying with your data, by sacrificing your privacy, and jeopardizing your mental health. If a platform provides alternatives, pick the paid option. Dopamine exhaustion can cost you more than $10 a month. Not even the treatment costs, but the opportunity cost.

    When you start looking for such alternatives, you'll notice that great things are happening in free and commercial software ecosystems in general. We're sometimes too imbued in big tech news and feel like everything's going bad for us, but we need to take note that that's mostly because ads and hype are intertwined. When you look beyond the billboards, there are many good people doing good things.

    ]]>
    <![CDATA[IPv6 for the remotely interested]]>I’ve known about IPv6 for the last two decades or so, but I’ve never gone beyond “an overengineered solution to the IPv4 address space problem”. IPv6 was even presented as “every atom could get its own IP address, no IP address shortages anymore

    ]]>
    https://ssg.dev/ipv6-for-the-remotely-interested-af214dd06aa7/6869fa0ab1ca250001b54482Tue, 16 Apr 2024 08:31:44 GMT

    I’ve known about IPv6 for the last two decades or so, but I’ve never gone beyond “an overengineered solution to the IPv4 address space problem”. IPv6 was even presented as “every atom could get its own IP address, no IP address shortages anymore”, but I didn’t know how true that was either. I occasionally saw an IPv6 address here and there because almost every device supports IPv6 today. I believe cellular network operators even default to it, so you’re probably reading this on a device that uses IPv6.

    Last week, I decided to learn about how IPv6 works under the hood, and I’ve learned quite a few interesting facts about it.

    Disclaimer: I’m not an expert on IPv6 or network engineering in general. This is the outcome of my personal reading journey over the last few weeks, and I’d love to be corrected if I made any mistakes. Read on :)

    IPv6 vs IPv4

    The name IPv6 used to confuse me because I thought IPv4 took its name from the four octets it used to represent 32-bits, so, IPv6 should have been called IP16. But I learned that it was really the version of the protocol. There were apparently IPv1, IPv2, and IPv3 before IPv4 came out. They were used to research the IP protocol internally, and later got replaced with IPv4 we use today. There was even a proposal for IPv5 in the 80’s that was intended to optimize realtime communications, but got discarded in favor of IPv6 which additionally solved the address space problem. That’s why IPv6 is called IPv6. It’s literally IP Version 6. There have even been attempts at creating IPv7, IPv8 and more, but all have been either obsoleted or shelved.

    Like IPv4, IPv6 protocol has an addressing scheme. IPv6 uses 128-bits for addresses instead of 32-bit IPv4 addresses. But, the difference in protocols are greater than address space sizes. Actually, IPv6 feels like an alien tech if you’ve only worked with IPv4 so far when you look at its quirky features such as:

    IPv6 has no subnet masks

    IPv6 supports CIDR addressing like IPv4, but from a user’s perspective, IPv6 addresses are way simpler: first half is Internet (global), the second half is local. That’s the suggested way to use IPv6 addresses anyway. So, when you visit a whatismyipwhatever web site, it shows your IP address like this:

    1111:2222:3333:4444:5555:6666:7777:8888

    But, your ISP only knows you as 1111:2222:3333:4444 and assigns that portion (/64) to you. The remaining half of the address is unique for every device on your network. ISP just forwards any packet that starts with 1111:2222:3333:4444 to your router, and your router transfers the packet to the device. So, the second half of the address, 5555:6666:7777:8888, let’s call that part INTERFACE_ID from now on, is unique to your device. That means, every device you have has a unique IPv6 address, and can be accessed individually from anywhere in the world, because:

    IPv6 has no NAT

    I used to think that you could do NAT with IPv6, but nobody did it because of potential backlash from HackerNews community. Apparently, that’s not the case. There’s apparently no published standard for NAT for IPv6. There is a draft proposal called NAT66, but it hasn’t materialized.

    NAT isn’t needed with IPv6 because it’s possible to have a separate globally accessible address for every device on Earth. That felt weird to me because NAT, despite how much you hate it when you want to play games online, gives you that warm feeling that your local devices are never accessible from outside unless you explicitly allow it using UPnP or port forwarding. It has that false sense of security which is really hard to shake off.

    The bitter truth is, NAT isn’t a security barrier. It’s just an alternative packet forwarding mechanism. Your IPv6 router should never forward connection attempts from outside to your local devices by default anyway. So, you get the same security without having NAT at all. As a matter of fact, it’s fascinating that you’re able to access every device on your local network with their IPv6 address without having to go through your router, or a separate VPN configuration if you wish to do so: just authenticate, that’s it. Hypothetically, a smart toothbrush in Istanbul, Turkey can connect directly to a temperature sensor in Ontario, Canada, and create one of the most diverse botnets on the planet.

    There is a security related catch with IPv6 though that comes with the luxury of having a separate IPv6 address per device: your devices can be fingerprinted and tracked individually. That’s bad for privacy. So, modern OS’s invented the concept of temporary IPv6 addresses that change INTERFACE_ID periodically. You can use your permanent IPv6 address for listening to connections from outside, but when establishing connections, your IPv6 address is shown with that secondary temporary address that changes frequently.

    Now, having mentioned not needing to go through hoops for access, another interesting feature of IPv6 is:

    IPv6 addresses are self-configured

    IPv6 protocol doesn’t need a DHCP server, or manual network configuration to determine IP address, subnet mask, and gateway address. A device can get an IP address without asking a centralized server. That is accomplished by a protocol called SLAAC. It gradually builds a device’s IPv6 address by following these steps:

    1. The operating system (specifically, the IPv6 stack of the OS) generates a 64-bit device identifier, usually random, let’s say 5555:6666:7777:8888 (chosen by a fair dice roll), and that makes up the INTERFACE_ID portion of your IPv6 address.
    2. The OS prefixes the INTERFACE_ID with fe80, the local only IPv6 network prefix. So, your IPv6 address is now: fe80::5555:6666:7777:8888. (Notice the “a::b” syntax; it means “there are all zero valued segments between ‘a’ and ‘b’”. More on that later)
    3. Your device now sends a packet to its designated neighbor multicast group on the local network to make sure that nobody else is using the same IPv6 address. That’s called Duplicate Address Detection (DAD). The chances of a duplicate address getting assigned is less than universe suddenly imploding due to a cataclysmic event, but that’s exactly when you don’t want to deal with duplicate IPv6 addresses and miss all the fun.
    4. Finally, the device sends the router (which, unlike IPv4, can always be reached with the multicast group address on IPv6 ff02::2) its acquired local address and asks for the actual prefix the router uses by sending a RS (Router Solicitation) ICMPv6 packet. After router responds with an RA (Router Advertisement) packet, it replaces fe80 with the actual prefix the router replies with, and starts using that as its permanent address. That’s now your IPv6 internet address.

    The advantage of stateless configuration is the reduced overhead on your router: it doesn’t have to maintain the IP configuration of every device on the network individually. That means better performance, especially in larger networks.

    IPv6 for the remotely interested
    This just happened. Explain this coincidence, atheists!

    IPv6 myths

    IPv6 comes with bold claims too. Let’s debunk them:

    Your device has one IPv6 address for every purpose

    I mean, yes, you use the same IPv6 address for both local and remote connections. But no, the “one IP address to rule them all, one IP address to find them” claim isn’t true. As I mentioned before, your device claims the ownership of multiple IPv6 addresses for different scopes like link-local (Remember fe80::) and Internet. Additionally, your device might acquire two different Internet IPv6 addresses too: permanent and temporary. Temporary IPv6 addresses are intended to preserve your privacy as they are rotated periodically. Permanent IPv6 addresses are primarily for servers which must have static IPv6 addresses.

    An IP address for every atom in the universe

    Not even close. There are about 2²⁷² atoms in the universe. Even Earth has 2¹⁶⁶ atoms, so we need at least 168-bits (octet-aligned) address space for them. The actual IPv6 address space is slightly smaller than 128-bits too: the first 16-bits are IANA reserved. You only have the remaining 112-bits to identify devices. That’s still a lot, way more than probably all devices we can produce on Earth in the next millenia, but no, we can’t give every atom its own IP address. But, we can give IPv6 addresses to every grain of sand on Earth. We can even fit them all inside a single /64 prefix.

    All in all, yes, IPv6 address space is vast regardless of how many arbitrary particles we can address with it.

    Universal connectivity of every device

    Yes, IPv6 has no NAT. So, that means no more port forwarding or address space to maintain. But, you still have to have a mechanism to open your device to connections from a remote host if you want to establish a direct connection. Remember, your router/firewall by default will prevent any connection attempt. What are you going to do?

    As with UPnP/IGD days, apps today still need to work with a protocol like PCP (Port Control Protocol) in order to open access to a port programmatically. So, it’s not like you suddenly have universal connectivity with global+local IPv6 addresses. You don’t have to set up manual port forwarding, but apps still need to work with the router in order to make themselves accessible.

    It’s not just the benefits of IPv6 being exaggerated, but there are cases where IPv6 turns out worse than IPv4 too:

    Downsides of IPv6

    There are several things that we take for granted in IPv4 world that IPv6 might make you nostalgic about, such as:

    You are at the mercy of your ISP to have subnets

    Since IPv6 has no NAT, many ISPs in United States default to forwarding only a single 64-bit prefix (usually called a “/64”) to your router. That means your router has no space left to put the subnet information into an IPv6 address. Remember: IPv6 addresses are auto-configured by devices, so, there is no way for a router to dictate those devices to use less than 64-bit local addresses. That means, your router would have no way to know which subnet to forward a packet to.

    Essentially, you’re in the mercy of ISPs to receive prefixes shorter than 64-bit so that your router can use the remaining bits to identify which subnet they need to go to. ISPs can actually afford giving home users at least 16 subnets by simply assigning 60-bit prefixes, but ISPs don’t do that for reasons unknown to me. Maybe the PTSD they had from IPv4 address space shortage made them greedy bastards? Or, they just want to make money by extorting customers. “Hey, if you want a shorter prefix, pay us more”. As far as I know, both Comcast Xfinity and AT&T give their home users a mere /64 prefix: one subnet.

    You might say that a home user may not need subnets at all, but, with the prevalence of IoT devices and our greater reliance of the security of our networks, isolating your untrusted devices is getting more important. RIPE, the European authority on IP address assignments, recommends a 56-bit prefix for residential ISP customers. That gives every customer 256 subnets, and that’s the greediest, the most conservative option that Europeans could come up with which an American can only dream of.

    Of course, you can configure IPv6 address of every device manually, and give them subnet identifiers this way, but that would be a huge undertaking, especially considering the overhead of adding new devices. Do you want to spend your retirement as a human DHCP server?

    IPv6 addresses need extra encoding in URIs

    Remember typing “http://192.168.0.1” on your browser and accessing your router settings? I do. Because “:” character is reserved for port numbers in the URI specification, it’s impossible to do the same using IPv6 addresses without additional encoding. In case you want to access a web page hosted on a device by its IPv6 address, you have to use the syntax: “http://[aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222]/path/?query”, notice the brackets around the address. But, that’s not even the worst part because:

    It’s impossible to memorize IPv6 addresses

    We’ve never been supposed to memorize IP addresses, but the reality is different. I’m still not sure about which address I can use reliably and consistenly to access my router on IPv6. I can’t memorize its full IP address, that’s for sure. mDNS helps, but it doesn’t always reliably work either.

    Hexadecimal is harder than regular numbers too. It’s like trying to memorize a Windows XP product activation code. What was that famous one? FCKGW-RHQQ2-??eh, whatever.

    Memorizing an IPv4 address is a transferable skill; “cross-platform” if you will. It’s even universal due to pervasive NAT: 192.168.1.1 most of the time. I didn’t have to look that up. Figuring out the IPv6 address of your router on an arbitrary device you have requires different skills.

    On the bright side, you now know that the rightmost 64-bit portion of an IPv6 address is always random, so, you can at least avoid assuming that it’s going to stay forever or supposed to make sense. You can even call that part BLABLA instead of INTERFACE_ID. You can memorize your /64 prefix and at least find out your router address, which is usually something like 1111:2222:3333:4444::1.

    IPv6 addresses are complicated

    Make no mistake, IPv4 addresses are complicated too. Did you know that 2130706433 is a valid IPv4 address? Or, 0x7F000001, 0177.0000.0000.0001 and 127.1 for that matter? Try pinging them on a shell if you don’t believe me. It’s hard to believe but, they’re all equivalent to 127.0.0.1.

    IPv6 addresses have a similar level of variety in representation. Here are some of their characteristics:

    • The representation of an IPv6 address consists of 8 hextets: sixteen bit hexadecimal groups canonically called segments. (“Hextet” is a misnomer for hexadectet, but too late now). Anyway, now hex tricks like this are possible:
    IPv6 for the remotely interested
    “face:b00c” I see what you did there.
    • Prefixing zeroes in hextets are not displayed. So, 2600:00ab is actually shown as 2600:ab.
    • As I mentioned before, hextets with zero values can completely be removed from the address and replaced with double colons. So, 2600:ab:0:0:1234:5678:90ab:cdef would be displayed as 2600:ab::1234:5678:90ab:cdef. See the double colons? That can only be done with the first batch of zero hextets though. So, 2600:ab:0:0:1234:0:0:cdef would still render like 2600:ab::1234:0:0:cdef. Also, you can’t compact just a single zero hextet. So, the zero in 2600:0:1234:5678:abcd:ef01:2345:6789 remains as is.
    • You can specifiy zone id: the network interface that you want to reach that address through with “%” suffix and a zone id. For example, you can be connected to a network over both WiFi and Ethernet, but may want to ping your router from LAN. In that case you append “%” to the address and add your zone id (network adapter identifier). Such as fe80::1%eth0 or fe80::1%3. The problem is, in addition to the brackets you need to use in IPv6 URIs, you must escape “%” to “%25” in your browser address bar or any other place where you need to use zone id in a URI.
    • IPv6 addresses can also be used to represent IPv4 addresses. So, you can ping 127.0.0.1 using IPv6 address syntax by prepending it with IPv4 translation prefix, and it’ll be regarded as an IPv4 address: ::ffff:127.0.0.1. But, that doesn’t mean your IPv4 requests will go through IPv6 network. That just tells the underlying networking stack to use an IPv4 connection instead. If you choose another prefix than ::ffff, the IPv4 portion will be made part of the last two hextets and you’ll connect that IP over IPv6 network. For example, 2600:1000:2000:3000::192.168.1.1 will be treated as 2600:1000:2000:3000::c0a8:101, the last two hextets being the hexadecimal equivalent of 192.168.1.1.

    These are all valid IPv6 addresses:

    • :: That’s all zeroes0:0:0:0:0:0:0:0.
    • 2600:: That’s an equivalent to 2600:0:0:0:0:0:0:0.
    • ::ffff:1.1.1.1 is an equivalent to 1.1.1.1 IPv4 address.
    • 2607:f8b0:4005:80f::200e is the address I get when I ping google.com. You know the drill; it’s equivalent to 2607:f8b0:4005:80f:0:0:0:200e. As you can see, Like Facebook, Google also took the hard road and decided to assign manually designated INTERFACE_ID ‘s to its IPv6 addresses. Godspeed.

    In the end, an IPv6 address you write on your address bar might look like this as a contrived example:

    https://[542b:b2ae:ed5c:cb5a:e38b:2c49:123:192.168.1.1%25eth3]

    No way I’m memorizing that.

    That all said, I loved learning about IPv6! The learning experience clarified a few things for me. For example, I didn’t know IPv6 addresses were self-configured with a stateless protocol. I didn’t know it had no NAT. I didn’t know the address space was just conveniently split in half.

    I wish we had a shortcut IPv6 address for our default gateway. I propose fe80::1. IETF, take note! :)

    I remember that IPv6 support in Windows 2000 was a big step when announced, and we all thought IPv6 would get adopted in a decade or so. Could we be more wrong? Yet, learning about it made me understood why it hasn’t caught on fast.

    IPv6 provides no benefit to end-users

    Despite how technologically advanced IPv6 is, IPv4 just works. It works even behind NAT, even behind multiple layers of NATs, even with its extremely cramped address space, cumbersome DHCP, and port forwarding. It keeps working. When people find a way that it doesn’t work, and can never work, somebody comes up and makes that work too.

    There’s probably a latency advantage of IPv6 not having NAT, but that’s not good enough to make a dent in user experience.

    Because IPv6 doesn’t provide any tangible benefit, users will never demand it, and they’ll just be pushed to it without them even knowing, like how we almost always use IPv6 on cellular internet nowadays.

    That means, when ISPs feel enough pressure from the limitations of IPv4, they’ll switch to IPv6 in an instant. No question about it.

    I wish IPv6 enabled some features that enabled a few distinct scenarios not possible with IPv4, so people could demand IPv6 to use them. Yet, I love the alienesque nature of IPv6 networks, and look forward to the time we fully abandon IPv4 and build everything around IPv6 instead.

    ]]>
    <![CDATA[Solving disinformation with Internet literacy]]>https://ssg.dev/solving-disinformation-with-internet-literacy-cd9781bc0da1/6869fa0ab1ca250001b54486Thu, 08 Jun 2023 02:09:31 GMT

    Fake people and fake news have existed on Internet since forever. They didn’t make such a terrible impact at the beginning because Internet in the 90’s lacked two things: users and amplification.

    Internet users were an elite minority in the 90’s, mostly consisted of university students. They could see through fake. Some couldn’t and that was okay too, because fake news couldn’t go far either because there was no “retweet” button. You had to be very intentional about whom you wanted that piece of information reach to.

    It took years for sensational sites (called ragebait today) like Bonsai Kitten or Manbeef to make an actual dent in the mainstream news. Even then, it didn’t matter, because the whole world didn’t move in synchrony to react to the news. It took years for people to discover the sites one by one, get offended personally or with a couple of friends, then figure out that they were actually fake, calm down and relax.

    For a long time, the most sensational fake news on Ekşi Sözlük, the social platform I founded in 1999, were fake celebrity deaths. There was no moderation for fake news on the web site, on the contrary the footprint said “Everything on this website is wrong” which put the onus on the user to verify the information.

    Fake celebrity deaths kept coming over the years, but people had started to be more careful. This kind of hands-on training created a new kind of Internet user: the literate.

    The literate would be aware that any person or any news on the Internet could be fake, and the onus were on them to verify if they are real any time they consume the content or communicate with someone. They could see the typos in a phishing mail as red flags, they could see the signals in fake news, they could see how Anita22 could be a man using a fake profile picture. They knew that the prince of Nigeria couldn’t care less to give you a penny, they knew how to do reverse image search.

    Solving disinformation with Internet literacy
    The literate

    Unfortunately, the floodgates opened when iPhone revolution came and suddenly everyone on the planet had Internet access from their pockets. This new generation of people, our parents, relatives, the illiterate so to speak, didn’t go through the similar process that the literate had. They were clueless, lacked the tools, and could easily be manipulated. We let the sheep out to the meadow full of wolves.

    That kind of user flow could still be managed well if we had slow and intentional sharing mechanics of the 90’s. You wanted to share something? You had to be very deliberate about it. You had to write an email, or forward it, but still you had to pick recipients yourself. Unfortunately, in 2010, we had already been part of the “amplification platforms” for years then. We call them social media today, but what they’re known for is their amplification mechanics. You consume the content from your little social bubble, but when you share it, it gets shared with the whole world. This asymmetric information flow could cause any kind of information regardless how true or credible to become a worldwide topic in mere hours.

    The illiterate lacked tools to deal with this. They were forced inside their bubble. If someone had corrected a fake news article, the victim of disinformation would have no way to know about it. Because their bubble wouldn’t convey that information.

    Because the illiterate are trapped in their bubble, they have no way to hone their skills about categorizing information themselves either. Their bubble consists of people mostly with similar levels of Internet literacy.

    We basically lack healthy community structures on Internet.

    I find Reddit one of the healthier platforms among the others in English-speaking world. Considering that how much it suffers from toxicity itself, the relativity of my statement begs emphasis. In fact, it resembles Ekşi Sözlük a lot in its community structure, but does some things better. For example, subreddits themselves can be great flow guards for information, and disinformation for that matter. Every subreddit is a broadcast channel and a community at the same time, causing people to focus their attention and efforts into only a single type of content. That leads them making fewer mistakes.

    The conversation structure on Reddit, the hierarchical threads are better than Twitter’s mentions or Facebook’s replies which are linear. Reddit’s style helps organizing information better along with opposing views. The visibility of content is influenced by people’s votes, so, people have lots of tools to deal with disinformation on Reddit, and moderation may not be even the most important part of it.

    There is a reason most controversial disinformation scandals appear on Twitter, because it has the most exponential amplification systems in place with as little controls as possible. There’s no downvote, no hierarchical discussion structure, no moderation hierarchy. Everything is designed to make content amplification easier: the retweet icon, retweets showing up on your main timeline with the same priority as original content, “trending topics” so you can retweet the most retweeted content even more. Everything becomes a feedback loop for more amplification of the content, not necessarily high quality content either. It just has to be controversial in some way.

    Because Twitter is “social media”, people with the most followers have the most reach. Only a minority actually gets the most engagement on the platform, and the majority barely gets anything at all. This hurts content quality. Reddit doesn’t have this problem because amplification dynamics doesn’t take number of followers into account. You can have zero followers yet still get upvoted to the front page that day if you had produced content the subreddit community likes. I know, Reddit still has a “follower” mechanism, but it’s not the backbone of how engagement works on the site.

    There are supposedly moderated communities which I think was an attempt to fix Twitter’s structural deficiencies, but the way they’re designed makes them pretty much useless. Should I share my content with the whole world through my followers, or just this small room of hundred people who cannot share my content even if they wanted to? I understand what problem they’re trying to solve: the uncontrolled amplification, but they fail to provide other ways for user to get engagement.

    Elon Musk seems to be trying to address this problem with “For You” tab on Twitter which tries to make you contact with people outside your bubble, and let those people to promote their content to the people who’re not following them. But, the effective result becomes is just another way of uncontrolled flow of information causing disinformation distribute in new ways in addition to the existing ones.

    Essentially, healthy communities are not profitable enough, and the engagement Rube Goldberg machine is fed with controversy. Controversy is the natural output of a for-profit social platform as it brings the most page views.

    Paid memberships can’t fix this issue either because there’s no way that it can get near to the income generated by ads. Even if they can match the income, they can’t match the growth. The investors want immediate growth.

    How do we fix this? Do we need more regulation? I don’t think so, because I don’t think people who design those regulations is capable to understand this problem better than the platforms themselves.

    I believe that we need more Internet literacy. We have to make Internet literacy perhaps a prerequisite for accessing Internet. We need to teach it in schools, in courses, on web sites, but we have to have a curriculum for Internet. Let’s teach children about fake news, fake people, how people can harm us over the Internet, how trolls seek engagement, how they can protect their private information, and what tools can be used to avoid harm. Let’s teach them to navigate Internet skillfully and deliberately.

    The literate will break this amplification chain because they will think twice before retweeting. They will research if they think the content seems off. Because of their education, they will ask for better platforms, they will migrate to them when they appear. The bad platforms will either have to change, or turn into a graveyard.

    ]]>
    <![CDATA[What are the strong sides of Windows?]]>https://ssg.dev/what-are-the-strong-sides-of-windows/688472edecb84b00013177b9Mon, 03 Oct 2022 07:00:00 GMT

    From a user’s point of view:

    • Hardware support. Windows is designed to support a vast hardware ecosystem, therefore has the most mature infrastructure for managing consumer hardware. For example, it’s probably the only desktop OS that can recover from a GPU driver failure and restart the driver instead of completely halting.
    • Great security features for such a widely deployed operating system. Like the GPU driver isolation example, drivers cannot mess with the system even though they have privileged status. As far as I know, it’s the only desktop OS with such a feature. It’s also the only OS that comes with a decent built-in antivirus/anti-malware.
    • Backwards compatibility. You can run most 32-bit Windows applications from early 90’s on a PC today with no changes. This is a huge deal, and Microsoft works hard to maintain that. That’s even applicable to device drivers to a certain extent.
    • Accessibility. Every piece of UI on Windows is accessible by keyboard. You can move, resize windows without touching the mouse. Linux is probably better than MacOS on that front, but on MacOS, a mouse is required, and that can be problematic when you need to do things using keyboard only.
    • Gaming. This is a bit like a self-fulfilled prophecy, as Windows is great for gaming because it’s popular, but that’s still the reality. Most games get released on Windows and provide the smoothest experience right after consoles (which one is also Windows-based). As an avid gamer, that’s an important factor for me.
    • This is mostly a personal preference but I find Windows desktop is the most usable and way superior to MacOS or Linux alternatives. It incorporates “docking” and “windowed” paradigms beautifully, with great HiDPI, fractional scaling, and multi-monitor support, along with super fast and usable virtual desktops. I know both MacOS and Linux supports those, but I’ve had encountered many problems on each regarding these, Windows is simply seamless.
    • Windows Subsystem for Linux (WSL) is an amazing environment for any use case that Windows can’t cover.
    • Many would disagree with me but I like having separate logical storage volumes like C: D:. I think that excluding volumes from directory enumeration provides a more usable model compared to a single root file system with everything in subdirectories. For example, I can store all my data in D:, and erasing files on C: wouldn’t be a problem at all. Compare that to Unix where you have to carefully unmount certain subdirectories before performing such an operation, otherwise your data is gone. Make no mistake, I think drive letters is still a primitive paradigm and less flexible than Unix paths, but I find them more intuitive.

    Ok, this sounded like an ad. So, what are the weak sides of Windows?

    • Windows is usually the third platform for many open source software, and the most neglected because how radically its APIs differ from Unix-based platforms. It requires extra effort to port an application to Windows instead of say, to MacOS or Linux due to API differences. Technologies like Cygwin, Msys2, and WSL make this less of a burden, but it’s still there.
    • Despite the introduction of Microsoft Store and WinGet (the command-line package manager), software installation experience is still mostly “download the installer and go through the setup steps”. I believe that Linux package management and the “drag’n’drop” installation experience of MacOS have been way superior for a long time. I’m looking forward for Windows catching up to those.
    • Windows 11 has come a long way about this, but MacOS is still excellent at restoring your work environment after a restart, even after a full OS upgrade. Windows 11 still struggles putting things back into place after a reboot.
    • After the SQL Blaster worm incident in the early 2000’s, Win32 API has been stripped off of certain advanced networking capabilities like Ethernet sniffing, or raw packet generation. This forces software to install their own device drivers to bypass those limitations, which isn’t ideal at all. WSL2 isn’t subject to such limitations, so it’s partly alleviated.
    ]]>
    <![CDATA[Hover is over]]>https://ssg.dev/hover-is-over-5ca728a01cde/6869fa0ab1ca250001b5448bFri, 10 Jun 2022 01:52:43 GMT

    This is my UI design advice inspired from “Eval is evil.” Stop relying on hover for revealing hidden actions, or providing essential information. Hover’s distracting, clunky, undiscoverable, inaccessible, and frustrating.

    Distracting

    When you reveal a certain part of your UI layout using hover, unhovering would hide the same UI. That might easily turn into a a flicker on the UI where the user’s scrolling or just moving the mouse pointer around. Remember the web page ads that popped up when you only hovered on a word? Remember those feelings? That feeling is exactly what made Anakin Skywalker turn to the dark side.

    Clunky

    To avoid the problem with distraction, UI designers add delays, so the hidden part is only revealed when you wait long enough on the certain region of the interface. That solves the distraction problem, but now you have a clunky UI that makes you wait much longer. Every time you have to use the same UI, you’re charged a non-distracting UI fee from your time.

    Undiscoverable

    There’s no universal UI language that says “this item can be hovered, and will do this if you do so.” You only accidentally discover a hoverable UI, or you hover over every UI element like the old LucasArts adventure games where you had to find the right pixel to pick up the right item to solve the puzzle. I loved those games but not the pixel finding parts.

    Inaccessible

    In addition to being undiscoverable, which is also an accessibility issue in itself, hover-based UI designs require mouse finesse. You must avoid moving the mouse too much or you might lose the hover. People have developed numerous techniques to prevent you from losing your hover status on a drop down menu item.

    You have to be able to target a certain area with mouse without accidentally click on it either. I’m sure operating systems provide certain fallback mechanisms to alleviate some of these issues, hover isn’t accessible by default. It’s hard to use.

    Some screen readers might use hover text information and present it in a meaningful way, but there are other, better suited ways for that like ARIA labels without relying over the hover itself.

    Frustrating

    Hover state can easily get lost by accidentally moving the mouse and that can cause the user to click on something else instead unintentionally. Similarly, losing hover would make you wait another second to see the UI again. It’s all prone to mistakes, accidents, tears, and screams.

    Besides, mobile devices can’t support hover because we haven’t invented such screen sensors yet. Mobile web browsers treat clicks as hovers. That means when you click an element that shows a different style when hovered, you’ll have to click on it once again, because your first click would only register as hover. Arguably, any sane designer would add conditionals to avoid such behavior on mobile, but that means there’s a disconnect between the UX provided by platforms which is an issue in itself. Users will have to learn different ways of using your app regardless how subtle the differences are, which is also frustrating.

    The hoverless way

    Just integrate the revealed portion of the interface into the main UI, or under a clickable action. You might think that it’s an extra step for UI interaction, but it’s actually faster than waiting for menu to appear.

    When is it acceptable to use hover?

    Ideally, never use hover. If you have to, use hover only to amplify the quality of the user’s experience, and never as the core of your user interaction flow. Your UI should be perfectly usable and accessible without hover.

    P.S. If you like my content, I have a book about programming tips; check out Street Coder.

    ]]>