This is indeed undefined behaviour.
If you look at one (quite ugly) implementation of strptime here
You see:
case 'y':
match_year_in_century:
/* Match year within century. */
get_number (0, 99, 2);
/* The "Year 2000: The Millennium Rollover" paper suggests that
values in the range 69-99 refer to the twentieth century. */
tm->tm_year = val >= 69 ? val : val + 100;
/* Indicate that we want to use the century, if specified. */
s.want_century = 1;
s.want_xday = 1;
break;
case 'Y':
/* Match year including century number. */
get_number (0, 9999, 4);
tm->tm_year = val - 1900;
s.want_century = 0;
s.want_xday = 1;
break;
So the year is read as a number from which 1900 subtracted. Years in struct tm count from 1900.
Whereas if your time-format was %y you would have got a sensible answer of 22 -> 2022
because
When a century is not otherwise specified, values in the range
[69,99] shall refer to years 1969 to 1999 inclusive, and values in the
range [00,68] shall refer to years 2000 to 2068 inclusive;
Why doesn't %Y fall back to %y behaviour for years <100?
Because it doesn't have to. It could but the wording of the standard doesn't require it.
So it would seem the safe thing to do when using strptime() is to check the number of digits before calling it and either reject 2-digit years or change the format specifier to %y if only 2-digits are provided.
Alternatively if you want it fall back to %y behaviour and you know your implementation behaves this way you could post process the year as in:
tm->tm_year = tm->tm_year < 0 ? tm->tm_year : tm->tm_year + 1900;
tm->tm_year = tm->tm_year >= 69 ? val : tm->tm_year + 100;
But then you are yourself relying on undefined behaviour.
You should also reject 3-digit years somehow as these will most likely be typos.
Also note the comment:
It is expected that in a future version of IEEE Std 1003.1-2001 the default century inferred from a 2-digit year will change. (This would apply to all commands accepting a 2-digit year as input.)