fix dataraces, add -race install, add load-gen. (#232)

The `Authz()` method of the WFE was racey. First because it didn't lock the authorizations and orders it was working with. Second because the handling of displaying authorization challenges was working with `acme.Challenge` objects owned by `core.Challenge`'s that should have been locked for reading but were not. This mean the VA would datarace with the WFE when updating a validated challenge status.

To prevent future occurrences `travis.yml` is updated to install Pebble with the race detector enabled, and to run Pebble such that it will exit non-zero if a race is detected.

Since `Chisel2.py` is single threaded the Boulder `load-generator` is used for a short duration to drive concurrent request traffic. In practice before fixing the dataraces I found this would crash Pebble <30s.

Resolves https://github.com/letsencrypt/pebble/issues/230
Resolves https://github.com/letsencrypt/pebble/issues/228
This commit is contained in:
Daniel McCarney 2019-04-15 13:53:25 -04:00 committed by Jacob Hoffman-Andrews
parent 8bc2d5654a
commit 22e0a4bcb4
5 changed files with 74 additions and 32 deletions

View File

@ -29,15 +29,16 @@ before_install:
install: install:
# Install `golangci-lint` using their installer script # Install `golangci-lint` using their installer script
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.15.0 - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.15.0
# Install `cover` and `goveralls` without `GO111MODULE` enabled so that we # Install tools without `GO111MODULE` enabled so that we
# don't download ct-woodpecker dependencies and just put the tools in our # don't download Pebble's deps and just put the tools in our
# gobin. # gobin.
- GO111MODULE=off go get golang.org/x/tools/cmd/cover - GO111MODULE=off go get golang.org/x/tools/cmd/cover
- GO111MODULE=off go get github.com/mattn/goveralls - GO111MODULE=off go get github.com/mattn/goveralls
- go install -v -mod=vendor ./... - GO111MODULE=off go get github.com/letsencrypt/boulder/test/load-generator
- go install -v -race -mod=vendor ./...
before_script: before_script:
- pebble & - GORACE="halt_on_error=1" pebble &
script: script:
- go mod download - go mod download
@ -50,6 +51,10 @@ script:
- goveralls -coverprofile=coverage.out -service=travis-ci - goveralls -coverprofile=coverage.out -service=travis-ci
# Perform a test issuance with chisel2.py # Perform a test issuance with chisel2.py
- REQUESTS_CA_BUNDLE=./test/certs/pebble.minica.pem python ./test/chisel2.py example.letsencrypt.org elpmaxe.letsencrypt.org - REQUESTS_CA_BUNDLE=./test/certs/pebble.minica.pem python ./test/chisel2.py example.letsencrypt.org elpmaxe.letsencrypt.org
# Run the load-generator briefly - note, because Pebble isn't using the
# load-generator's mock DNS server none of the issuances will succeed. This
# step is performed just to shake out data races with concurrent requests.
- load-generator -config ./test/config/load-generator-config.json > /dev/null
deploy: deploy:
- provider: script - provider: script

View File

@ -49,10 +49,10 @@ type Order struct {
// An Authorization is created for each identifier in an order // An Authorization is created for each identifier in an order
type Authorization struct { type Authorization struct {
Status string `json:"status"` Status string `json:"status"`
Identifier Identifier `json:"identifier"` Identifier Identifier `json:"identifier"`
Challenges []*Challenge `json:"challenges"` Challenges []Challenge `json:"challenges"`
Expires string `json:"expires"` Expires string `json:"expires"`
// Wildcard is a Let's Encrypt specific Authorization field that indicates the // Wildcard is a Let's Encrypt specific Authorization field that indicates the
// authorization was created as a result of an order containing a name with // authorization was created as a result of an order containing a name with
// a `*.`wildcard prefix. This will help convey to users that an // a `*.`wildcard prefix. This will help convey to users that an

View File

@ -120,6 +120,7 @@ type Authorization struct {
URL string URL string
ExpiresDate time.Time ExpiresDate time.Time
Order *Order Order *Order
Challenges []*Challenge
} }
type Challenge struct { type Challenge struct {

View File

@ -0,0 +1,26 @@
{
"plan": {
"actions": [
"newAccount",
"newOrder",
"fulfillOrder",
"finalizeOrder"
],
"rate": 10,
"runtime": "10s",
"rateDelta": "1/10s"
},
"directoryURL": "https://localhost:14000/dir",
"domainBase": "com",
"challengeStrategy": "random",
"httpOneAddrs": [":5002"],
"tlsAlpnOneAddrs": [":5001"],
"dnsAddrs": [":8053"],
"fakeDNS": "127.0.0.1",
"regKeySize": 2048,
"certKeySize": 2048,
"regEmail": "loadtesting@letsencrypt.org",
"maxRegs": 20,
"maxNamesPerCert": 20,
"dontSaveState": true
}

View File

@ -1179,10 +1179,7 @@ func (wfe *WebFrontEndImpl) makeChallenges(authz *core.Authorization, request *h
// Lock the authorization for writing to update the challenges // Lock the authorization for writing to update the challenges
authz.Lock() authz.Lock()
authz.Challenges = nil authz.Challenges = chals
for _, c := range chals {
authz.Challenges = append(authz.Challenges, &c.Challenge)
}
authz.Unlock() authz.Unlock()
return nil return nil
} }
@ -1496,12 +1493,13 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(
} }
// prepAuthorizationForDisplay prepares the provided acme.Authorization for // prepAuthorizationForDisplay prepares the provided acme.Authorization for
// display to an ACME client. // display to an ACME client. It assumes the `authz` is already locked for
func prepAuthorizationForDisplay(authz acme.Authorization) acme.Authorization { // reading by the caller.
func prepAuthorizationForDisplay(authz *core.Authorization) acme.Authorization {
// Copy the authz to mutate and return // Copy the authz to mutate and return
result := authz result := authz.Authorization
identVal := result.Identifier.Value identVal := result.Identifier.Value
// If the authorization identifier has a wildcard in the value, remove it and // If the authorization identifier has a wildcard in the value, remove it and
// set the Wildcard field to true // set the Wildcard field to true
if strings.HasPrefix(identVal, "*.") { if strings.HasPrefix(identVal, "*.") {
@ -1509,20 +1507,20 @@ func prepAuthorizationForDisplay(authz acme.Authorization) acme.Authorization {
result.Wildcard = true result.Wildcard = true
} }
// If the authz isn't pending then we need to filter the challenges displayed // Build a list of plain acme.Challenges to display using the core.Challenge
// to only those that were used to make the authz valid || invalid. // objects from the authorization.
if result.Status != acme.StatusPending { var chals []acme.Challenge
var chals []*acme.Challenge for _, c := range authz.Challenges {
// Scan each of the authz's challenges c.RLock()
for _, c := range result.Challenges { // If the authz isn't pending then we need to filter the challenges displayed
// Include any that have an associated error, or that are status valid // to only those that were used to make the authz valid || invalid.
if c.Error != nil || c.Status == acme.StatusValid { if result.Status != acme.StatusPending && (c.Error == nil && c.Status != acme.StatusValid) {
chals = append(chals, c) continue
}
} }
// Replace the authz's challenges with the filtered set chals = append(chals, c.Challenge)
result.Challenges = chals c.RUnlock()
} }
result.Challenges = chals
// Randomize the order of the challenges in the returned authorization. // Randomize the order of the challenges in the returned authorization.
// Clients should not make any assumptions about the sort order. // Clients should not make any assumptions about the sort order.
@ -1553,6 +1551,12 @@ func (wfe *WebFrontEndImpl) Authz(
return return
} }
authz.Lock()
defer authz.Unlock()
authz.Order.RLock()
orderAcctID := authz.Order.AccountID
authz.Order.RUnlock()
// If the postData is not a POST-as-GET, treat this as case A) and update // If the postData is not a POST-as-GET, treat this as case A) and update
// the authorization based on the postData // the authorization based on the postData
if !postData.postAsGet { if !postData.postAsGet {
@ -1562,7 +1566,7 @@ func (wfe *WebFrontEndImpl) Authz(
return return
} }
if authz.Order.AccountID != existingAcct.ID { if orderAcctID != existingAcct.ID {
wfe.sendError(acme.UnauthorizedProblem( wfe.sendError(acme.UnauthorizedProblem(
"Account does not own authorization"), response) "Account does not own authorization"), response)
return return
@ -1596,7 +1600,7 @@ func (wfe *WebFrontEndImpl) Authz(
return return
} }
if authz.Order.AccountID != account.ID { if orderAcctID != account.ID {
response.WriteHeader(http.StatusForbidden) response.WriteHeader(http.StatusForbidden)
wfe.sendError(acme.UnauthorizedProblem( wfe.sendError(acme.UnauthorizedProblem(
"Account authorizing the request is not the owner of the authorization"), "Account authorizing the request is not the owner of the authorization"),
@ -1608,7 +1612,7 @@ func (wfe *WebFrontEndImpl) Authz(
err := wfe.writeJSONResponse( err := wfe.writeJSONResponse(
response, response,
http.StatusOK, http.StatusOK,
prepAuthorizationForDisplay(authz.Authorization)) prepAuthorizationForDisplay(authz))
if err != nil { if err != nil {
wfe.sendError(acme.InternalErrorProblem("Error marshalling authz"), response) wfe.sendError(acme.InternalErrorProblem("Error marshalling authz"), response)
return return
@ -1787,7 +1791,13 @@ func (wfe *WebFrontEndImpl) updateChallenge(
return return
} }
if authz.Order.AccountID != existingAcct.ID { authz.RLock()
authz.Order.RLock()
orderAcctID := authz.Order.AccountID
authz.Order.RUnlock()
authz.RUnlock()
if orderAcctID != existingAcct.ID {
response.WriteHeader(http.StatusUnauthorized) response.WriteHeader(http.StatusUnauthorized)
wfe.sendError(acme.UnauthorizedProblem( wfe.sendError(acme.UnauthorizedProblem(
"Account authenticating request is not the owner of the challenge"), response) "Account authenticating request is not the owner of the challenge"), response)