Skip to content

avoid signed overflow computing start offset in AndroidZoneInfoSource#356

Merged
mkruskal-google merged 4 commits into
google:masterfrom
rajath201:android-tzdata-offset-overflow
Jul 23, 2026
Merged

avoid signed overflow computing start offset in AndroidZoneInfoSource#356
mkruskal-google merged 4 commits into
google:masterfrom
rajath201:android-tzdata-offset-overflow

Conversation

@rajath201

Copy link
Copy Markdown
Contributor

AndroidZoneInfoSource::Open decodes each index entry's absolute data offset as data_offset + Decode32(ebuf + 40), where both operands come from a loaded, untrusted tzdata database. std::int_fast32_t is 32 bits on Bionic, macOS, and Windows, so a crafted index with data_offset and an entry offset near INT32_MAX overflows the addition before the start < 0 guard ever runs (UBSan flags 2147483647 + 2147483646).

Do the sum in int_fast64_t before the range check, the same way the TZif reader in Load() guards its decoded values. Real tzdata offsets stay well under INT32_MAX, so valid zones are unaffected; only the hostile case changes, and it now fails the bounds check cleanly instead of relying on undefined behavior.

Comment thread src/time_zone_info.cc
if (start < 0 || length < 0) break;
ebuf[40] = '\0'; // ensure zone name is NUL terminated
if (strcmp(name.c_str() + pos, ebuf) == 0) {
if (fseek(fp.get(), static_cast<long>(start), SEEK_SET) != 0) break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we then also be checking that the newly-widened start fits into a long?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, done. long is still 32 bits on LLP64 targets, so the cast at the fseek() could truncate a large start; the guard now also rejects entries with start > std::numeric_limits<long>::max(). Where long is 64 bits the check folds away, and gcc/clang stay quiet about it under the CI's -Wall/-Wextra.

Comment thread src/time_zone_info.cc Outdated
static_cast<std::int_fast64_t>(data_offset) + Decode32(ebuf + 40);
const std::int_fast32_t length = Decode32(ebuf + 44);
if (start < 0 || length < 0) break;
if (start < 0 || start > std::numeric_limits<long>::max() || length < 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like somewhere along the way (2017), we failed to add #include <limits> to this file, so we might as well do it now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, added to the include block.

Comment thread src/time_zone_info.cc Outdated
const std::int_fast32_t length = Decode32(ebuf + 44);
if (start < 0 || length < 0) break;
if (start < 0 || start > std::numeric_limits<long>::max() || length < 0)
break; // fseek() takes a long

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new comment only applies to one of the conditionals, so I'd either just remove the comment, or separate the "fseek() takes a long" conditional into its own statement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with the split. The long-range check is now its own statement with the comment on it, and the original start/length check reads the same as before the PR.

Comment thread src/time_zone_info.cc Outdated
if (fread(ebuf, 1, sizeof(ebuf), fp.get()) != sizeof(ebuf)) break;
const std::int_fast32_t start = data_offset + Decode32(ebuf + 40);
const std::int_fast64_t start =
static_cast<std::int_fast64_t>(data_offset) + Decode32(ebuf + 40);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following an idea from #360, we're better off using brace initialization instead of static_cast for a widening integer conversion. That is, std::int_fast64_t{data_offset}.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, switched to std::int_fast64_t{data_offset}. Nice property that it'd fail to compile if the conversion ever became narrowing.

@devbww devbww left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Thanks.

We'll have to wait for a Google owner to initiate the tests.

@mkruskal-google
mkruskal-google merged commit 6b9d0e0 into google:master Jul 23, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants