This is an archive of the discontinued LLVM Phabricator instance.

[LLD][Windows]Feature "checksum" for Windows PE
ClosedPublic

Authored by Qfrost911 on Dec 2 2022, 2:50 AM.

Details

Summary

There is a field named checksum in PE, in order to check file whether be corrupted. Sometimes, VisualStudio will defaultly send this option, RELEASE, to lld. So, a error will be raised

lld-link : error : could not open '/RELEASE': no such file or directory

The general idea is to generate the binary and then recalculate the checksum before outputting it.

Diff Detail

Event Timeline

Qfrost911 created this revision.Dec 2 2022, 2:50 AM
Herald added a project: Restricted Project. · View Herald TranscriptDec 2 2022, 2:50 AM
Qfrost911 requested review of this revision.Dec 2 2022, 2:50 AM
Herald added a project: Restricted Project. · View Herald TranscriptDec 2 2022, 2:50 AM
aaron.ballman resigned from this revision.Dec 2 2022, 12:16 PM

I don't know enough about lld to be an appropriate reviewer for this, but the PE bits look correct based on my (limited) understanding of PE.

lld/COFF/Writer.cpp
621

Something to double-check: is BaseAddress going to be aligned properly for dereferencing p? I believe this is fine, but it's worth verifying.

mstorsjo added inline comments.Dec 2 2022, 12:32 PM
lld/COFF/Writer.cpp
617

What is oldChecksum initialized to here, practically?

633

Is size + 1 here meant for rounding up? Shouldn’t it then be sizeof(uint16_t)-1 (even though that’s literally equal to 1)?

I have reviewed the source code of imagehlp.CheckSumMappedFile, which was leaked in nt5src, it is indeed rounding up.

And the oldCheckSum , it was preserved in Windows source code also. Although it is useless now, I think it is reserved for future, because it is very easy to add checks internally, so I copied this part of the code.

thieta accepted this revision.Dec 6 2022, 8:26 AM

Looks good to me!

lld/COFF/Writer.cpp
603

I am a little surprised that we don't have a function for calculating this checksum already. We are not duplicating code with this?

This revision is now accepted and ready to land.Dec 6 2022, 8:26 AM
thieta requested changes to this revision.Dec 6 2022, 8:27 AM

Ah! Wait - we need tests for this as well!

This revision now requires changes to proceed.Dec 6 2022, 8:27 AM

Oh well, I've tested a dozen samples using petool and they are all correct.

And I want to know, why this revision didn't pass the check in x64debian, I think my revision's format is correct.

tonic added a subscriber: tonic.Dec 8 2022, 5:26 PM

Hello! Please hold off on approving this patch as its pending legal review. Thanks!

To be honest, I didn't know what I should do.

Should I add some test sets to the vision?

Hello! Please hold off on approving this patch as its pending legal review. Thanks!

Hi, did you mean should I keep waiting, or should I add some tests?

Hello! Please hold off on approving this patch as its pending legal review. Thanks!

Hi, did you mean should I keep waiting, or should I add some tests?

You should keep waiting -- the LLVM Foundation lawyers need some time to review the legal aspects of the contribution (which can take a fair amount of time, especially given the holiday season). That said, I'd be curious to hear what caused the foundation lawyer involvement in the first place given that PE32 is a publicly documented format (https://learn.microsoft.com/en-us/windows/win32/debug/pe-format); as a reviewer, it'd be helpful for me to know what to watch out for in this space.

tonic added a comment.Dec 23 2022, 9:35 AM

Thanks so much for your patience. The concern was over the reference to leaked source code. This patch can be accepted assuming that no code was taken/copied from any copyrighted source.

I deem that it is impossible to leak source code because the "checksum" only occupies only a DWORD. If reviewers think there is no problem, please accept the patch so that I can submit this commit.

I deem that it is impossible to leak source code because the "checksum" only occupies only a DWORD. If reviewers think there is no problem, please accept the patch so that I can submit this commit.

This about the implementation of checksum. You mentioned that you had looked at leaked nt5 source code. As long as no code has been taken or copied from copyrighted nt5 source, then it is ok from a legal perspective to be included.

aganea added a subscriber: aganea.EditedDec 26 2022, 10:21 AM

I deem that it is impossible to leak source code because the "checksum" only occupies only a DWORD. If reviewers think there is no problem, please accept the patch so that I can submit this commit.

This about the implementation of checksum. You mentioned that you had looked at leaked nt5 source code. As long as no code has been taken or copied from copyrighted nt5 source, then it is ok from a legal perspective to be included.

If someone was to take a look at that code, they would see it is different from what it is currently written in this patch.

The algorithm used for the PE checksum is essentially RFC1071, as documented here and here. We already have an implementation in libc, here, but I wouldn't touch or move it.

I think the whole algorithm can be implemented more simply just as it is described in RFC1071:

void Writer::writePEChecksum() {
  if (!config->writeCheckSum) {
    return;
  }

  // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#checksum
  uint32_t *buf = (uint32_t *)buffer->getBufferStart();
  uint32_t size = (uint32_t)(buffer->getBufferSize());

  coff_file_header *coffHeader =
      (coff_file_header *)((uint8_t *)buf + dosStubSize + sizeof(PEMagic));
  pe32_header *peHeader =
      (pe32_header *)((uint8_t *)coffHeader + sizeof(coff_file_header));

  uint64_t sum = 0;
  uint32_t count = size;
  uint16_t *addr = (uint16_t *)buf;

  while (count > 1) {
    sum += *addr++;
    count -= 2;
  }

  // Add left-over byte, if any
  if (count > 0)
    sum += *(unsigned char *)addr;

  // Fold 32-bit sum to 16 bits
  while (sum >> 16) {
    sum = (sum & 0xffff) + (sum >> 16);
  }

  sum += size;
  peHeader->CheckSum = sum;
}

And probably a comment that mentions a link to the RFC.

I've tested with a simple program:

C:\git\llvm-project>cat main.cpp
int main() { return 0; }

C:\git\llvm-project>stage1\bin\clang-cl main.cpp /link /RELEASE /Brepro

C:\git\llvm-project>stage1\bin\llvm-readobj.exe --all main.exe | grep CheckSum
  CheckSum: 0x1D26D

(which is the same value as the implementation in the current patch)

lld/COFF/Writer.cpp
617

@Qfrost911 I wouldn't worry about the old checksum, in practical terms is should be always 0. Its maybe just the kernel that needs it for hotpatching perhaps? In our case it doesn't matter I think.

Qfrost911 updated this revision to Diff 485340.Dec 26 2022, 8:47 PM

Thanks. This method is really concise and clear. If don't consider the possibility of future use, such as adding other verifications, it is advisable to remove this part of complex code.

If someone was to take a look at that code, they would see it is different from what it is currently written in this patch.

Thanks for adding that context info!

The algorithm used for the PE checksum is essentially RFC1071, as documented here and here. We already have an implementation in libc, here, but I wouldn't touch or move it.

Thanks!

I think the whole algorithm can be implemented more simply just as it is described in RFC1071:

void Writer::writePEChecksum() {
[...]
  uint16_t *addr = (uint16_t *)buf;

I think this needs to use ulittle32_t *addr instead (as the images are written in that form anyway), so that we get the same checksum when running the linker on big-endian hosts. (I would expect that we do have some buildbot configs that run on big endian.)

And probably a comment that mentions a link to the RFC.

Indeed, please add at least a mention of RFC1071! And we do need a testcase too, but I see that you're working on the prerequisites for that in D140555.

Thanks.

But I have some questions. Why using "ulittle32_t" rather than "ulittle16_t".

And to be honest, I don't know how to achieve a test. It is very difficult to calc a checksum using lit tools.

But I have some questions. Why using "ulittle32_t" rather than "ulittle16_t".

Sorry, that was a typo - it should indeed be ulittle16_t.

And to be honest, I don't know how to achieve a test. It is very difficult to calc a checksum using lit tools.

It should be quite straightforward.

Have a look at some existing test, e.g. lld/test/COFF/heap.test seems like a good example. First the test creates an object file from a stored yaml file (the exact contents doesn't matter much here). Then you would link one output without the /release flag, which should not set any checksum. After this, run llvm-readobj and inspect that the checksum in the output file is 0x0. (You may want to check for 0x0{{$}} to make sure that it doesn't accidentally match a substring like 0x01234.) Then run another link command with the /release flag, and check that it produces the expected checksum.

The exact value of the checksum in the test isn't that important - you don't need to verify the calculation of that separately. You've checked that the linker does the right thing in actual use - then just see what checksum value you get from the test input, and look for that as reference in the testcase. That way, we will notice if something changes (so that the checksum algorithm returns a different value than the expected one).

Qfrost911 updated this revision to Diff 485591.Dec 29 2022, 1:29 AM

Thank you very much.

But I still have some confusion. You mentioned if we post "/RELEASE", we need to compare the value read from llvm-readobj and an expected checksum. But how do I calc an expected checksum using lit tools? Adding RFC1071 in lit test? We cannot determine a unique checksum for each specific yaml, because some values in PE will be changed every time compilation. These values can impact the checksum.

But I still have some confusion. You mentioned if we post "/RELEASE", we need to compare the value read from llvm-readobj and an expected checksum. But how do I calc an expected checksum using lit tools? Adding RFC1071 in lit test?

No, you don't need to reimplement it - like I said, it's enough to just check against a hardcoded test value.

We cannot determine a unique checksum for each specific yaml, because some values in PE will be changed every time compilation. These values can impact the checksum.

If that's the case, we should add flags that make the output deterministic.

There's a flag /Brepro which you can add, which changes the timestamp field into a deterministic hash-based timestamp. Or you can add /timestamp:0. With either of those flags in place, the output of e.g. the tests in the heap.test file I suggested as example, all are completely deterministic, producing exactly the same binary each time.

Oh, I understand. Thank you very much. And I think I should wait reviewers accept D140555 so that I can use llvm-readobj to add tests.

Oh, I understand. Thank you very much. And I think I should wait reviewers accept D140555 so that I can use llvm-readobj to add tests.

Yes, that would be best.

Looks good generally.

@tonic Does your veto still hold? Can we move ahead with this patch?

lld/COFF/Writer.cpp
620

Could you please expand the comment a bit? Something like « The PE checksum algorithm, implemented as suggested in RFC1071 »

Qfrost911 updated this revision to Diff 485779.Dec 31 2022, 8:46 AM

add commit

tonic added a comment.Jan 1 2023, 6:26 AM

Looks good generally.

@tonic Does your veto still hold? Can we move ahead with this patch?

I have already provided feedback on the legal aspect and assuming those conditions are met (sounds like they are), then this patch is ok to go in.

Looks good generally.

@tonic Does your veto still hold? Can we move ahead with this patch?

I have already provided feedback on the legal aspect and assuming those conditions are met (sounds like they are), then this patch is ok to go in.

Thanks. I deem that this patch includes a standard algorithm "RFC1071", and it is different between nt5src. Moreover, GCC achieves this feature, which means it is public.

Additionally, I want to know why the CI points my patch have some errors under "x64 debian failed". I have used clangd to format "lld/COFF/Driver.cpp" before I created the patch.

thieta accepted this revision.Jan 2 2023, 12:24 AM

Looks like it's ready to go to me. Maybe take a look at the comments inline in the review and mark them as done or reply to them, there were some questions in there that I couldn't see any replies to.

This revision is now accepted and ready to land.Jan 2 2023, 12:24 AM
Qfrost911 marked 6 inline comments as done.Jan 2 2023, 1:16 AM
This revision was landed with ongoing or failed builds.Jan 2 2023, 1:20 AM
This revision was automatically updated to reflect the committed changes.