This is an archive of the discontinued LLVM Phabricator instance.

[libFuzzer] Initial implementation of weighted mutation leveraging during runtime.
ClosedPublic

Authored by kodewilliams on Jul 20 2018, 3:36 PM.

Details

Summary

Added functions that calculate stats while fuzz targets are running and give
mutations weight based on how much new coverage they provide, and choose better
performing mutations more often.

Patch by Kodé Williams (@kodewilliams).

Diff Detail

Event Timeline

kodewilliams created this revision.Jul 20 2018, 3:36 PM
Herald added subscribers: Restricted Project, llvm-commits. · View Herald TranscriptJul 20 2018, 3:36 PM

Left a few comments with high level concerns and some less important ones as well.
Did you use clang-format on this? If not I may have to start using vscode myself since clang-format had no suggested changes to this. Good job.

lib/fuzzer/FuzzerFlags.def
166

Please put some kind of explanation here.

lib/fuzzer/FuzzerLoop.cpp
562

Is this assertion useful?

563

Let's not use magic numbers. Please use a constant instead of 10,000.

lib/fuzzer/FuzzerMutate.cpp
525

Better not to clobber the original mutator. I would do:

if (UseMutationWeights)
  ...
else
  ...

instead of what we have here.

592

I think this should be SetMutationStats since it sets something rather than returns something.
I would also add a comment explaining what this method does since it changes the vector it accepts.

596

Although we use percentages in when printing stats, I'm not sure we need to do it here (and in other parts of this CL), since the only other thing that will see this number is your own code.

607

nit: Please add a period to the end of this comment and all others that are missing one.

610

I don't think I like the fact that 1 is the default weight. But maybe I misunderstand what its implications are.

I think because 1 is the default weight, a totally useless mutation would be weighted higher than a mutation that is useful less than 1/1000 times.

Less importantly:

Let's use a constant instead of 1 a magic number.

Also, I think it would be better if the body of this if were on its own line (for consistency with the else) but clang-format agrees with you.
I don't think you should change this unless matt or kcc says to.

611

I think the placement of this comment is awkward. Maybe put on the same line as else.

613

I'm somewhat unsure why we divide by SumOfStats, can you explain this to me please?

615

Think this loop can be eliminated and that SumOfWeights can be calculated in the previous loop.
Actually, won't it always be 1000?

620

Helpful comment! I think it can be improved in 2 ways:

  1. Move it outside the function.
  2. Change the order so that it says the main purpose first and then says what happens to lowest weighted mutations.
624

I don't think this line does what you want. Did you have any warnings compiling your code?

RandomMark and MutationWeights are both unsigned values so RandomMark - MutationWeights[i] will never be less than 0.
I think comparing them directly will work better than subtracting them (which I think is harder to understand).

627

Is the purpose of this to ensure that we return an index on the last iteration?
I need to think about this technique more but I am not sure it is fair (ie: things with equal weight have equal chance of being picked).

629

I would add an assert(false) so that it is obvious if it does happen.

lib/fuzzer/FuzzerMutate.h
103

IMO the word "Records" is better than "Gets" which implies something returned.
I also think this sentence is easier to understand with a comma after "coverage"

173

super nit: I think the language here can be slightly improved. Maybe this instead:

// Used to weight mutations based on usefulness.
174

I don't think MutationWeights should store size_t since it isn't really storing sizes.
This size_t seems to have infected a lot of other code in this CL so removing it will be nice.

175

I don't find it great to use UseMutationWeights to special case the case when we haven't assigned any weights yet.
It would be cleaner if GetMutationStats and AssignMutationWeights were called before any mutations happened, so that we don't need to special case but everything works as if we did.

test/fuzzer/fuzzer-weightedmutations.test
4 ↗(On Diff #156623)

How long does it take to do 100k runs? Does this UnitTest take long?

Also, what behavior does this test verify? The only thing I can tell is that weighted mutations doesn't crash anything.
I think it would be nice to have a test of something more substantial if possible.

metzman added inline comments.Jul 23 2018, 8:07 AM
lib/fuzzer/FuzzerMutate.cpp
611

I'm also unsure what three decimal places this comment is referring to. We don't round anywhere here do we?

627

Actually I think it is fair. But let's have a comment explaining how this technique works.

kodewilliams marked 21 inline comments as done.

I used git version of clang-format with -p flag for the non-trivial changes, but I believe there is also a VS Code plugin that includes clang-format, so you should try it @metzman

Dor1s added inline comments.Jul 24 2018, 7:01 AM
lib/fuzzer/FuzzerFlags.def
166

please format the line to be consistent with style of the rest of the file

lib/fuzzer/FuzzerLoop.cpp
41

why not static?

41

I think it may have a better name, maybe something like kUpdateMutationWeightRuns

559

Check the bool flag first, then check number of runs

lib/fuzzer/FuzzerMutate.cpp
23

Do we use this anywhere outside of MutationDispatcher::AssignMutationWeights? Looks like we don't, please use a local variable inside the function for that.

67

This won't use Mutate_CustomCrossOver, will it? We should probably use Mutators.size() here and debug what was the root cause of the test that was failing for you.

594

Mismatch with line 69

613

+1

622

That's not good, we should not loop through the array and implement our own weighted random selector, there are STL classes for that. See how corpus distribution is implemented, for an example: https://github.com/llvm-mirror/compiler-rt/blob/master/lib/fuzzer/FuzzerCorpus.h#L294

I think you can try std::discrete_distribution instead, it looks sufficient for our purpose.

test/fuzzer/fuzzer-weightedmutations.test
4 ↗(On Diff #156623)

+1

metzman added inline comments.Jul 24 2018, 7:20 AM
lib/fuzzer/FuzzerMutate.cpp
592

Please make this comment say explicitly that it modifies the argument it is passed.

596

Since this isn't a percentage anymore, the name UsefulPercentage needs to be updated.
Some general advice: After making suggested changes in a code review, you should go back through your code to see if it still makes sense, and then update it where the suggested changes have amde it necessary to do so.
Also, the parentheses don't do anything here.

605

SumOfStats can be calculated in the loop in SetMutationStats right?

610

How about instead of having a default usefulness stat of 0 and a default MutationWeight of 1 we just use 1/(100 * 1000) as the default stat.

613

Please leave a comment explaining why we are adding kDefaultMutationWeight here.
Also, since this else body is multiple lines, please put curly braces around it.
Another thing: let's use a const instead of magic number 1000.
Some general advice about code reviews: If a reviewer comments that a change should be made, you should go through your code and see if that same change needs to be made elsewhere.

nit: inner parentheses not actually needed.

615

Doesn't it always end up as 1000 (now 1000+Mutators.size()*kDefaultMutationWeight) at the end of the loop?

Also, if a reviewer leaves a question/comment that looks like it needs a response, (ie: not a request to change something) please reply to it instead of marking it Done.
It's impossible to properly review code if the author is unresponsive.
Even if the question was discussed offline (in this particular case it wasn't), it is good to summarized the answer when for reviewers who weren't in the offline conversation.
Do this for every question I have asked in this review.

620

Does this need to return a size_t?
Also, was this inspired by https://stackoverflow.com/a/1761646 ?

lib/fuzzer/FuzzerMutate.h
119

This comment is phrased somewhat differently from the others, maybe phrase it like so:
Returns the Index of a mutation...

metzman requested changes to this revision.Jul 24 2018, 11:04 AM

This CL is still broken, it doesn't actually do what it is intended to do.
I recommend testing it more thoroughly (manually, or preferably using written tests) to ensure that it accomplishes it what is intended to, in at least a few cases, before starting the next review.
I personally would build one or two fuzzers with this change, and use print statements or GDB to verify that this change is doing what is intended (using sanitizer_keep_symbols=true as a gn arg will let you print variable values in gdb), and then write some tests.
This is good advice for most code reviews, really they should only contain broken code if you are asking the reviewer why certain code is broken.

lib/fuzzer/FuzzerLoop.cpp
41

+1, the name we originally picked together is incorrect.

559

+1.
When you have a condition composed of two or more conditions, you can take advantage of short circuiting by putting the more efficient (especially if likely to be false) conditions first.
You can read more on this here.

lib/fuzzer/FuzzerMutate.cpp
593

This method is kind of weird. It is only called once with an argument called Stats which is a file-scope variable that it then shadows.
Please change the method so it no longer shadows the variable (ie: change the name of the argument or less preferably, change the method to not take any argument at all).
I'm also unsure it's worth keeping as its own function.

597

This is a serious bug, it pretty much breaks the CL.
The size of Stats on the first iteration is Mutators.size() not 0. Pushing to the back simply doubles the size of the array so every mutator ends up with a default weight.

lib/fuzzer/FuzzerMutate.h
96

Please use Doxygen style comments (ie: ///) like the other methods in this file.

test/fuzzer/fuzzer-weightedmutations.test
4 ↗(On Diff #156623)

I think it will be hard to verify some of the behavior that I have observed is broken in this CL with an integration test. You may want to use a unittest for this purpose.

This revision now requires changes to proceed.Jul 24 2018, 11:04 AM

This is good advice for most code reviews, really they should only contain broken code if you are asking the reviewer why certain code is broken.

Obviously we can't always write perfect code, but in the above comment, I mean that the code we put up for review should work as intended in simple cases.

kodewilliams retitled this revision from [libFuzzer] Initial implementation of weighted mutation leveraging during runtime. to [libFuzzer] Initial implementation of weighted mutation leveraging during runtime. (NOT READY FOR REVIEW).Jul 24 2018, 11:26 AM
kodewilliams marked 17 inline comments as done and 2 inline comments as done.
kodewilliams retitled this revision from [libFuzzer] Initial implementation of weighted mutation leveraging during runtime. (NOT READY FOR REVIEW) to [libFuzzer] Initial implementation of weighted mutation leveraging during runtime..

Unit test giving problems to write due to most members being private. Addressed all other comments.

kodewilliams marked 4 inline comments as done.Jul 27 2018, 10:57 AM
kodewilliams added inline comments.
lib/fuzzer/FuzzerMutate.cpp
597

Oversight on my end, not sure how I missed this so many times reading it. Stats should be cleared before these push_back instructions are executed.

605

It was suggested that SumOfStats be declared locally in AssignMutationWeights.

610

I feel like that works, it's close enough to zero to show basically minimal usefulness but isn't actually zero so it eliminates the case of me setting the default weight to 1.

611

We don't round anything, just when printing but even then that's just the format specifier rounding.

613

I divide by SumOfStats in order to normalize the numbers, since the usefulness percentages are ratios of themselves and not a total, the only way to determine which one is better than the other is by finding how much of the total usefulness each mutation accounts for.

615

Using discrete_distribution, I no longer need the SumOfWeights. And will do.

620

I had the original thought to decrement each mutation stat until it got to zero (we had discussed it in person). Then I realized it would not work in a case where the number of runs was not specified, so I got to searching and someone had the same problem I had (https://medium.com/@peterkellyonline/weighted-random-selection-3ff222917eb6). Inspired by this Medium post.

In MutateImpl, by default it uses Rand(Mutators.size()) to choose a Mutator index and Rand returns a size_t, hence my choice to also return size_t.

metzman added inline comments.Jul 27 2018, 12:00 PM
lib/fuzzer/FuzzerMutate.cpp
23

Please add a comment to explain the significance of 100 and 1000 (frankly i don't know what the purpose is of either since we don't actually round anything).

585

I like that you went to fix something based on comments you saw in on the review even if they weren't explicit.
But in general it's better not to refactor unrelated code in a CL. I'll accept it though.
This assignment statement is starting to look pretty bad, please make use a regular if else instead of ternary.

kodewilliams marked 7 inline comments as done.
Dor1s added a comment.Jul 27 2018, 1:43 PM

Haven't looked at the test yet, as the code needs a lot of clean up.

lib/fuzzer/FuzzerLoop.cpp
41

Repeating my previous comment:

why not static?

lib/fuzzer/FuzzerMutate.cpp
23

+1, what is it for?

66

nit: end the comment with a period

524–529

How will this work during first 10000 runs? Do we have a uniform distribution by default?

lib/fuzzer/FuzzerMutate.h
113

nit: "Returns" -> "Prints" or "Outputs"

Dor1s added inline comments.Jul 27 2018, 1:43 PM
lib/fuzzer/FuzzerMutate.cpp
581–582

It would be much safer to use Stats.size() here instead of Mutators.size() since you're using i to access elements in Stats, not in Mutators.

582

Move Printf call to the next line please. Also, this stat::... syntax is used for final stats. If we decide to print something during runtime, we should probably follow the style of the other log messages.

583

Again, overcomplication. We should start with some initial weights, and later on only overwrite those.

585

+1

585

Actually, that seems to be unnecessary complicated. Initially, we should have some default values in Stats. Probably zero, since none of the stats have been useful at that point. Later on, we should be able to overwrite default Stats value with a new one. That's it, there is no way we'll have to put the default value back.

Also, I don't find it good to check whether TotalCount is 0 or not on every run. Let's just initialize it with 1 in the very beginning, so it will never be 0 and it always will be safe to use that value as a divisor.

588

I don't understand why we need + kDefaultMutationWeight, as well as why we multiply the value by 1000.

590

This code is duplicated in MutationDispatcher::PrintMutationStats, can you please have it in a one place and call it from different places as needed.

600

I'd ask you to move SumOfStats += Stat; to the next line just to follow the style, but actually you should use std::accumulate instead of manual looping through the values.

609

It would be very inefficient to create a new random generator every time when we need to select an index. There is Random Rand(Seed); declared in the FuzzerDriver and I believe all the other code is using it, please use it as well.

610

Also, it's not the first time it looks like you've just copied the code from somewhere without even changing variable names to follow project's style. I guess, https://en.cppreference.com/w/cpp/numeric/random/discrete_distribution this time.

Also, please don't forget to run clang format before uploading a CL.

615

This is an overkill, we never change sizes of Mutators and MutationWeights from the very beginning of the fuzzing. If you want to assert that their sizes are equal, somewhere after FuzzerMutate.cpp:68 would be the best place, but that wouldn't make any sense since you've just called resize there. Just remove the assert and return SelectIndex(gen).

kodewilliams marked 18 inline comments as done.
lib/fuzzer/FuzzerMutate.cpp
23

It is just there to represent a usefulness ratio that is near to but not entirely useless. So that it still gets weight instead of calculating to 0.

524–529

AssignMutationWeights is called initially and sets all the mutation weights to the default, therefore they would be all be weighted the same and have the same probability before 10k runs.

588

I add the default to ensure that mutations that are any amount more useful than the default get weighted higher in the distribution. No longer multiplying by 1000.

test/fuzzer/fuzzer-weightedmutations.test
4 ↗(On Diff #156623)

Real: 0m 0.157s
User: 0m 0.036s
Sys: 0m 0.076s

metzman added inline comments.Jul 30 2018, 3:54 PM
lib/fuzzer/FuzzerMutate.cpp
33–36

Can you leave a comment here about why we are starting with a UsefulCount and a TotalCount of 1?

37–38

Why do ChangeASCIIInt and ChangeBinInt start with a UsefulCount of 0 when everything else starts with 1?

602

I don't think we need to multiply by 1.0, right? I don't think it (or the parentheses) does anything, please remove them if I am correct.

606

Kode, I don't think this is what Max meant. I believe that here you need to use the rand obtained by calling GetRand.
It's possible, though unlikely, that not doing this was causing the test flakiness you were seeing before.
I would run the previously flaky test in a loop and ensure that the tests pass every time, since if there is any flake we don't know about, it will be more painful to get rid of later rather than now.

metzman added inline comments.Jul 30 2018, 3:56 PM
test/fuzzer/fuzzer-weightedmutations.test
4 ↗(On Diff #156623)

Amount of time LGTM.

Dor1s added inline comments.Jul 30 2018, 5:27 PM
lib/fuzzer/FuzzerMutate.cpp
23

That doesn't seem right to me. Let's say we have a tough target -- where it's hard to reach any new coverage. In fact, we have a lot of them, when we use a full corpus.

So, after running for a while, it finally finds a couple useful mutations, all of them get weights, say, 10^(-6) (again, totally possible), while all "useless" mutations get the default weight of 10^(-5). That would mean, "useless" mutations would be chosen much more often than "useful" ones.

IMO, the better approach would be something like what we've discussed in the past:

  1. make random decision whether we use weighted mutations or default selection, 80% vs 20% should be fine. Once start testing, can change to 90 vs 10 or any other proportion
  2. if weighted mutations was chosen, call WeightedIndex(); otherwise, use default case

That approach would be universal as it doesn't use any magic numbers which correspond to a particular target, as your 100*1000 can behave very differently with targets having different speed.

37–38

+1

588

That won't work well for all targets. If mutations stats are very low numbers, that default mutation weight constant will bias the selection towards more uniform distribution, rather than giving more favor to more useful mutations.

594

here and on line 603, what's the point of multiplying by 1.0?

606

Yes, don't create a new Random object, use an existing one from FuzzerDriver. That's crucial for keeping deterministic behavior, as Jonathan has pointed out.

kodewilliams marked 11 inline comments as done.
metzman added inline comments.Jul 30 2018, 5:37 PM
lib/fuzzer/FuzzerMutate.cpp
23

I think it will be harder to determine if this technique is useful with the 80/20 strategy.
If the concern is that the weight is too high, why don't we use the smallest positive double as the default?

Dor1s added inline comments.Jul 31 2018, 11:26 AM
lib/fuzzer/FuzzerMutate.cpp
23

I think it will be harder to determine if this technique is useful with the 80/20 strategy.

Why will it be harder? That percentage would just control the factor of how strongly our strategy affects fuzzing process. We should still be able to see either a negative a positive impact. Changing the distribution (e.g. use 90/10 after 80/20) would multiple that impact a little bit.

If the concern is that the weight is too high, why don't we use the smallest positive double as the default?

I can't think of a value that would work well for both cases when useful mutations have stats like 10^(-3) and 10^(-6). We should avoid relying on anything that depends on fuzz targets speed / complexity of finding a new input. We already have magic threshold of 10000, let's not add more of those.

524–529

With my proposal it will be something like:

// Even when use weighted mutations, fallback to the default selection in 20% of cases.
if (Options.UseWeightedMutations && Rand(100) < 80) 
  M = &Mutators[WeightedIndex()];
else
  M = &Mutators[Rand(Mutators.size())];
kodewilliams marked 11 inline comments as done.
kodewilliams added inline comments.Jul 31 2018, 3:57 PM
lib/fuzzer/FuzzerMutate.cpp
23

@metzman @Dor1s PTAL. Made changes and ran tests. Test in question is no longer flaky (ran about 50 times) and ran two experiments locally. In both cases, the corpus grew more with the option enabled :) so maybe a good sign.

594

Both UsefulCount and TotalCount are uint64_t but Stats is a vector of doubles, so I multiply by 1.0 to make sure it gets calculated as double.

Dor1s added inline comments.Jul 31 2018, 4:21 PM
lib/fuzzer/FuzzerMutate.cpp
594

It's an obscure way to cast a value to another type. Please use static_cast<double>(Mutators[i].UsefulCount) instead.

kodewilliams marked 2 inline comments as done.
Dor1s added a comment.Aug 1 2018, 8:54 AM

I don't like the test as it only tests that we do not completely break libFuzzer, but doesn't test the feature itself. I'll play with some ideas locally, will share those if anything works out. Otherwise, I guess we'll proceed with this test.

lib/fuzzer/FuzzerMutate.cpp
33–36

"zero division" -> "division by zero"

66

add ", init with uniform weights distribution." to the comment please

67

Let's remove kDefaultMutationWeight variable, just use 1.0 here.

68

We should start with 0 values in Stats, remove kDefaultMutationStat and leave Stats.resize(Mutators.size() here.

522

Please do Mutator *M = nullptr; just to be safe in case there ever will be any bug and the pointer won't be initialized.

608

We should keep the distribution object inside of MutationDispatcher class and update it only every kUpdateMutationWeightRuns, not for every weighted index retrieval.

lib/fuzzer/FuzzerMutate.h
104

Let's rename this method to UpdateMutationStats, that name would make more sense.

174

This line and line 175 feel slightly inconsistent, both should be MutationWeights and MutationStats or just Weights and Stats. I'd prefer the latter, as we're already inside MutationDispatcher class and clearly deal with mutations here.

174

Actually, we don't need MutationWeights here, we need only the distribution. Could you please do the following:

  1. Remove this MutationWeights member
  1. Add Distribution member (also see my comment for WeightedIndex() function)
  1. Rename MutationDispatcher::AssignMutationWeights() to MutationDispatcher::UpdateDistribution()
  1. In MutationDispatcher::UpdateDistribution() create a local vector Weights and update it using Stats value as you do now
  1. Update the Distribution using the Weights, e.g. see how it's done for the corpus distribution (FuzzerCorpus.h:294)
kodewilliams marked 13 inline comments as done.
Dor1s added a subscriber: kcc.

Left a couple minor comments. Looks good otherwise. Still not happy with the test, but can't think of anything better so far.

Matt, please take a look when you get a change.

lib/fuzzer/FuzzerMutate.cpp
599

This variable is not needed until line 605. And may be not needed at all if we do an early return on line 603. Please declare it between lines 603 and 605.

600

We don't need to do this, just pass Mutators.size()to constructor of Vector<double> Weights. Default weight should be 0, so Vector<double> Weights(Mutators.size()); would be enough

kodewilliams marked 2 inline comments as done.
Dor1s accepted this revision.Aug 2 2018, 7:51 AM

LGTM

morehouse added inline comments.Aug 2 2018, 8:56 AM
lib/fuzzer/FuzzerMutate.cpp
524–529

use -> using

526

Maybe if (... && Rand(5)) would be simpler.

605

Is normalization necessary? I think discrete_distribution might do this internally.

606

I think you can do Distribution.param(Weights) rather than reconstructing. Might be faster.

lib/fuzzer/FuzzerMutate.h
103–111

What is the "most previous run"? Please clarify comment

Dor1s added inline comments.Aug 2 2018, 9:21 AM
lib/fuzzer/FuzzerMutate.cpp
606

Oh, right. Thanks for spotting, Matt! Kode, please remove lines 603-605 and try passing Stats to the distribution.

kodewilliams marked 5 inline comments as done.
morehouse added inline comments.Aug 2 2018, 10:53 AM
lib/fuzzer/FuzzerMutate.cpp
602

Does Distribution.param(Stats) work instead?

lib/fuzzer/FuzzerMutate.h
104

There's nothing inside UpdateMutationStats that refers to the number 10,000.

Maybe something like "Recalculates mutation stats based on latest run data."

test/fuzzer/fuzzer-weightedmutations.test
6 ↗(On Diff #158793)

This really only tests that there's no major regression with the flag on. If this flag performs better under some circumstances, we should add a test to demonstrate that.

morehouse added inline comments.Aug 2 2018, 10:56 AM
test/fuzzer/fuzzer-weightedmutations.test
6 ↗(On Diff #158793)

Also, we can probably add this to fuzzer-mutationstats.test instead.

kodewilliams marked 2 inline comments as done.Aug 2 2018, 11:20 AM
kodewilliams added inline comments.
lib/fuzzer/FuzzerMutate.cpp
602

Please see other comment on Distribution.param(Weights).

606

One param function doesnt take arguments (returns a param_type object) and the other one sets the new params based on a passed in param_type object, but there would have to be another discrete_distribution object from which the param_type object would be obtained so I think that's why they do it this way in https://cs.corp.google.com/piper///depot/google3/third_party/llvm/llvm/projects/compiler-rt/lib/fuzzer/FuzzerCorpus.h?rcl=206864844&l=294

Quite literally I would have to do Distribution.param(Distribution.param()) but I'm not sure that would update the distribution.

morehouse added inline comments.Aug 2 2018, 11:35 AM
lib/fuzzer/FuzzerMutate.cpp
601

SumOfStats doesn't need to be calculated.

kodewilliams marked an inline comment as done.
morehouse accepted this revision.Aug 2 2018, 1:44 PM
Dor1s added inline comments.Aug 2 2018, 1:52 PM
lib/fuzzer/FuzzerMutate.cpp
601

Matt, we needed SumOfStats to be non zero to ensure that we have at least some distribution. Until that moment is reached, we should be using the initial weights. I like this simplification though, but in that case we'll have to initialize UsefulCount with 1, so that initial Stats will be 1 / 1 for every mutation.

morehouse added inline comments.Aug 2 2018, 2:15 PM
lib/fuzzer/FuzzerMutate.cpp
601

Either approach SGTM.

kodewilliams marked 6 inline comments as done.

All comments addressed @morehouse

Dor1s updated this revision to Diff 158843.Aug 2 2018, 3:14 PM

Getting ready to land.

Dor1s edited the summary of this revision. (Show Details)Aug 2 2018, 3:19 PM
Dor1s updated this revision to Diff 158847.Aug 2 2018, 3:28 PM
Dor1s edited the summary of this revision. (Show Details)

Rebase

This revision was not accepted when it landed; it landed in state Needs Review.Aug 2 2018, 3:30 PM
This revision was automatically updated to reflect the committed changes.