Writing

Feed Software, technology, sysadmin war stories, and more.

Thursday, January 26, 2023

Tonight's rabbit hole: time math and 32 bit longs

I find some funny rabbit holes sometimes. Tonight, it went like this. Ubiquiti released a new version of the software for their USG devices because they had this thing where their dhcpv6-pd implementation could be exploited to run arbitrary commands by someone sitting in the right spot on the network (i.e., out your "WAN" port).

It's been a good while since they put out a new build for these devices, and I wanted to know what else changed. I find that when companies supposedly ship a "just a security fix" patch, they usually end up shipping far more, and probably break stuff too. (I'm still bitter about the 2020-002 "security update" for Macs.)

Anyway, it got me thinking: can you diff these things? Turns out, you sure can. It's two squashfs filesystems, so mount 'em and diff 'em, and dig through the results, and... hey.

/opt/vyatta/sbin/dhcpv6-pd-response.pl:
 
         if (defined $domain) {
             $domain =~ s/\.\s+$//;
+            $domain =~ s/[^A-Za-z0-9.]+/-/g;
             $dn = $domain;
         } else {
             $dn = "";

Yeah. That's what changed. So there's that. *facepalm*. Then I got bored and kept looking through the output to see what else happened. That's when I saw that the entirety of /etc/shadow changed. A bunch of numeric values changed, like this:

-root:!:18920:0:99999:7:::
+root:!:19369:0:99999:7:::

I had to look it up to be sure, but that's the "date of last password change". Divide them by 365 and you'll realize one of them is about 51 years, and the other one is about 53 years. So, 2021, and 2023 - the dates of the previous release and the new release, respectively. Their release process obviously rebuilds the shadow file.

But that's not the end of the rabbit hole. Thinking of last week's time post, I started looking at that number. It's so small. It fits into 16 bits (but not 15). I wondered what sort of type they were using to hold it. Into the shadow source I went.

The first thing I found was something called strtoday(). It looks like this (adjusted a bit to fit here):

long strtoday (const char *str) {
        time_t t;
[...]
        t = get_date (str, NULL);
        if ((time_t) - 1 == t) {
                return -2;
        }
        /* convert seconds to days since 1970-01-01 */
        return (long) (t + DAY / 2) / DAY;
}

Uh huh. It returns a long. On a 32 bit machine, a long is 4 bytes, and it's still going to be 4 bytes even after glibc does their "time_t is now 64 bits" thing that's coming down the pipe eventually. longs aren't going to change.

So, when does this break? It turns out... 12 hours BEFORE everything else blows up. "DAY" is defined in the source as (24L*3600L), so 86400 - the number of seconds in a day. It's taking half of that (so 43200 - 12 hours worth of seconds) and is adding it to the value it gets back from get_date. That makes it blow up 12 hours early.

2038-01-18 15:14:08Z is when that code will start returning negative numbers. That'll be fun and interesting.

Remember, the actual "end times" for signed 32 bit time_t is 12 hours later: 2038-01-19 03:14:08Z.

The lesson here is: if you take a time and do math on it and shove it into another data type, you'd better make sure it won't overflow one of those types that *won't* be extended between now and then.

...

$ cat t.cc
#include <stdio.h>
#include <sys/time.h>
 
#include <cinttypes>
 
#define DAY (24L*3600L)
 
long strtoday_tt(time_t t) {
  return (long) (t + DAY / 2) / DAY;
}
 
int main() {
  printf("2147440447 -> %ld\n", strtoday_tt(2147440447));
  printf("2147440448 -> %ld\n", strtoday_tt(2147440448));
  return 0;
}
$ ./t
2147440447 -> 24855
2147440448 -> -24855