Skip to content

Commit

Permalink
Fix a privacy-impacting bug in ThresholdedResult() function of dpagg.…
Browse files Browse the repository at this point in the history
…Count (#101)

Fix a privacy-impacting bug in ThresholdedResult() function of dpagg.Count that leads to slightly higher than intended delta. The bug is a result of converting floating-point threshold to an integer. Consider the following to see how the bug would occur:

Assume the noisy count is 37 and the threshold computed from noise parameters & threshold delta is 37.3. This means that we should not be returning the result. However, converting the threshold (37.3) to an int64 truncates the decimal part per go specification, making it 37. Threshold (37) is smaller than or equal to noisy result (37), so we return the result.

To fix this, we round the threshold up to the nearest integer before converting it to an int64.

Although this problem did not exist for dpagg.BoundedSumInt64, we modify the code there for consistency.
  • Loading branch information
miracvbasaran committed Feb 7, 2022
1 parent ec0b439 commit 6e74aef
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 22 deletions.
13 changes: 9 additions & 4 deletions go/dpagg/count.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,18 @@ func (c *Count) Result() int64 {
return c.noisedCount
}

// ThresholdedResult is similar to Result() but applies thresholding to the
// result. So, if the result is less than the threshold specified by the noise
// mechanism, it returns nil. Otherwise, it returns the result.
// ThresholdedResult is similar to Result() but applies thresholding to the result.
// So, if the result is less than the threshold specified by the parameters of Count
// as well as thresholdDelta, it returns nil. Otherwise, it returns the result.
//
// Note that the nil results should not be published when the existence of a
// partition in the output depends on private data.
func (c *Count) ThresholdedResult(thresholdDelta float64) *int64 {
threshold := c.Noise.Threshold(c.l0Sensitivity, float64(c.lInfSensitivity), c.epsilon, c.delta, thresholdDelta)
result := c.Result()
if result < int64(threshold) {
// Rounding up the threshold when converting it to int64 to ensure that no DP guarantees
// are violated due to a result being returned that is less than the fractional threshold.
if result < int64(math.Ceil(threshold)) {
return nil
}
return &result
Expand Down
16 changes: 13 additions & 3 deletions go/dpagg/count_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,14 +339,14 @@ func TestCountResultSetsStateCorrectly(t *testing.T) {
}

func TestCountThresholdedResult(t *testing.T) {
// ThresholdedResult outputs the result when it is greater than the threshold (5 using noNoise)
// ThresholdedResult outputs the result when it is greater than the threshold (5.00001 using noNoise)
c1 := getNoiselessCount()
for i := 0; i < 10; i++ {
c1.Increment()
}
got := c1.ThresholdedResult(tenten)
if got == nil || *got != 10 {
t.Errorf("ThresholdedResult(%f): when 10 addings got %v, want 10", tenten, got)
t.Errorf("ThresholdedResult(%f): after 10 entries got %v, want 10", tenten, got)
}

// ThresholdedResult outputs nil when it is less than the threshold
Expand All @@ -355,7 +355,17 @@ func TestCountThresholdedResult(t *testing.T) {
c2.Increment()
got = c2.ThresholdedResult(tenten)
if got != nil {
t.Errorf("ThresholdedResult(%f): when 2 addings got %v, want nil", tenten, got)
t.Errorf("ThresholdedResult(%f): after 2 entries got %v, want nil", tenten, got)
}

// Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil.
c3 := getNoiselessCount()
for i := 0; i < 5; i++ {
c3.Increment()
}
got = c3.ThresholdedResult(tenten)
if got != nil {
t.Errorf("ThresholdedResult(%f): after 5 entries got %v, want nil", tenten, got)
}
}

Expand Down
2 changes: 1 addition & 1 deletion go/dpagg/dpagg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (noNoise) AddNoiseFloat64(x float64, _ int64, _, _, _ float64) float64 {
}

func (noNoise) Threshold(_ int64, _, _, _, _ float64) float64 {
return 5
return 5.00001
}

// If noNoise is not initialized with a noise distribution, confidence interval functions will return a default confidence interval, i.e [0,0].
Expand Down
16 changes: 10 additions & 6 deletions go/dpagg/sum.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,19 @@ func (bs *BoundedSumInt64) Result() int64 {
return bs.noisedSum
}

// ThresholdedResult is similar to Result() but applies thresholding to the
// result. So, if the result is less than the threshold specified by the noise
// mechanism, it returns nil. Otherwise, it returns the result.
// ThresholdedResult is similar to Result() but applies thresholding to the result.
// So, if the result is less than the threshold specified by the parameters of
// BoundedSumInt64 as well as thresholdDelta, it returns nil. Otherwise, it returns
// the result.
//
// Note that the nil results should not be published when the existence of a
// partition in the output depends on private data.
func (bs *BoundedSumInt64) ThresholdedResult(thresholdDelta float64) *int64 {
threshold := bs.Noise.Threshold(bs.l0Sensitivity, float64(bs.lInfSensitivity), bs.epsilon, bs.delta, thresholdDelta)
result := bs.Result()
// To make sure floating-point rounding doesn't break DP guarantees, we err on
// the side of dropping the result if it is exactly equal to the threshold.
if float64(result) <= threshold {
// Rounding up the threshold when converting it to int64 to ensure that no DP guarantees
// are violated due to a result being returned that is less than the fractional threshold.
if result < int64(math.Ceil(threshold)) {
return nil
}
return &result
Expand Down
25 changes: 17 additions & 8 deletions go/dpagg/sum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -868,29 +868,38 @@ func TestBoundedSumFloat64ResultSetsStateCorrectly(t *testing.T) {
}

func TestThresholdedResultInt64(t *testing.T) {
// ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise)
// ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise)
bs1 := getNoiselessBSI()
bs1.Add(1)
bs1.Add(2)
bs1.Add(3)
bs1.Add(4)
got := bs1.ThresholdedResult(5)
got := bs1.ThresholdedResult(0.1)
if got == nil || *got != 10 {
t.Errorf("ThresholdedResult(5): when 1, 2, 3, 4 were added got %v, want 10", got)
t.Errorf("ThresholdedResult(0.1): when 1, 2, 3, 4 were added got %v, want 10", got)
}

// ThresholdedResult outputs nil when it is less than the threshold
bs2 := getNoiselessBSI()
bs2.Add(1)
bs2.Add(2)
got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold()
got = bs2.ThresholdedResult(0.1)
if got != nil {
t.Errorf("ThresholdedResult(0.1): when 1,2 were added got %v, want nil", got)
}

// Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil.
bs3 := getNoiselessBSI()
bs3.Add(2)
bs3.Add(3)
got = bs3.ThresholdedResult(0.1)
if got != nil {
t.Errorf("ThresholdedResult(5): when 1,2 were added got %v, want nil", got)
t.Errorf("ThresholdedResult(0.1): when 2,3 were added got %v, want nil", got)
}
}

func TestThresholdedResultFloat64(t *testing.T) {
// ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise)
// ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise)
bs1 := getNoiselessBSF()
bs1.Add(1.5)
bs1.Add(2.5)
Expand All @@ -905,9 +914,9 @@ func TestThresholdedResultFloat64(t *testing.T) {
bs2 := getNoiselessBSF()
bs2.Add(1)
bs2.Add(2.5)
got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold()
got = bs2.ThresholdedResult(0.1)
if got != nil {
t.Errorf("ThresholdedResult(5): when 1, 2.5 were added got %v, want nil", got)
t.Errorf("ThresholdedResult(0.1): when 1, 2.5 were added got %v, want nil", got)
}
}

Expand Down

0 comments on commit 6e74aef

Please sign in to comment.