Skip to content

Commit 6e74aef

Browse files
Fix a privacy-impacting bug in ThresholdedResult() function of dpagg.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.
1 parent ec0b439 commit 6e74aef

File tree

5 files changed

+50
-22
lines changed

5 files changed

+50
-22
lines changed

go/dpagg/count.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,18 @@ func (c *Count) Result() int64 {
173173
return c.noisedCount
174174
}
175175

176-
// ThresholdedResult is similar to Result() but applies thresholding to the
177-
// result. So, if the result is less than the threshold specified by the noise
178-
// mechanism, it returns nil. Otherwise, it returns the result.
176+
// ThresholdedResult is similar to Result() but applies thresholding to the result.
177+
// So, if the result is less than the threshold specified by the parameters of Count
178+
// as well as thresholdDelta, it returns nil. Otherwise, it returns the result.
179+
//
180+
// Note that the nil results should not be published when the existence of a
181+
// partition in the output depends on private data.
179182
func (c *Count) ThresholdedResult(thresholdDelta float64) *int64 {
180183
threshold := c.Noise.Threshold(c.l0Sensitivity, float64(c.lInfSensitivity), c.epsilon, c.delta, thresholdDelta)
181184
result := c.Result()
182-
if result < int64(threshold) {
185+
// Rounding up the threshold when converting it to int64 to ensure that no DP guarantees
186+
// are violated due to a result being returned that is less than the fractional threshold.
187+
if result < int64(math.Ceil(threshold)) {
183188
return nil
184189
}
185190
return &result

go/dpagg/count_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,14 +339,14 @@ func TestCountResultSetsStateCorrectly(t *testing.T) {
339339
}
340340

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

352352
// ThresholdedResult outputs nil when it is less than the threshold
@@ -355,7 +355,17 @@ func TestCountThresholdedResult(t *testing.T) {
355355
c2.Increment()
356356
got = c2.ThresholdedResult(tenten)
357357
if got != nil {
358-
t.Errorf("ThresholdedResult(%f): when 2 addings got %v, want nil", tenten, got)
358+
t.Errorf("ThresholdedResult(%f): after 2 entries got %v, want nil", tenten, got)
359+
}
360+
361+
// Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil.
362+
c3 := getNoiselessCount()
363+
for i := 0; i < 5; i++ {
364+
c3.Increment()
365+
}
366+
got = c3.ThresholdedResult(tenten)
367+
if got != nil {
368+
t.Errorf("ThresholdedResult(%f): after 5 entries got %v, want nil", tenten, got)
359369
}
360370
}
361371

go/dpagg/dpagg_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (noNoise) AddNoiseFloat64(x float64, _ int64, _, _, _ float64) float64 {
6161
}
6262

6363
func (noNoise) Threshold(_ int64, _, _, _, _ float64) float64 {
64-
return 5
64+
return 5.00001
6565
}
6666

6767
// If noNoise is not initialized with a noise distribution, confidence interval functions will return a default confidence interval, i.e [0,0].

go/dpagg/sum.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,19 @@ func (bs *BoundedSumInt64) Result() int64 {
254254
return bs.noisedSum
255255
}
256256

257-
// ThresholdedResult is similar to Result() but applies thresholding to the
258-
// result. So, if the result is less than the threshold specified by the noise
259-
// mechanism, it returns nil. Otherwise, it returns the result.
257+
// ThresholdedResult is similar to Result() but applies thresholding to the result.
258+
// So, if the result is less than the threshold specified by the parameters of
259+
// BoundedSumInt64 as well as thresholdDelta, it returns nil. Otherwise, it returns
260+
// the result.
261+
//
262+
// Note that the nil results should not be published when the existence of a
263+
// partition in the output depends on private data.
260264
func (bs *BoundedSumInt64) ThresholdedResult(thresholdDelta float64) *int64 {
261265
threshold := bs.Noise.Threshold(bs.l0Sensitivity, float64(bs.lInfSensitivity), bs.epsilon, bs.delta, thresholdDelta)
262266
result := bs.Result()
263-
// To make sure floating-point rounding doesn't break DP guarantees, we err on
264-
// the side of dropping the result if it is exactly equal to the threshold.
265-
if float64(result) <= threshold {
267+
// Rounding up the threshold when converting it to int64 to ensure that no DP guarantees
268+
// are violated due to a result being returned that is less than the fractional threshold.
269+
if result < int64(math.Ceil(threshold)) {
266270
return nil
267271
}
268272
return &result

go/dpagg/sum_test.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -868,29 +868,38 @@ func TestBoundedSumFloat64ResultSetsStateCorrectly(t *testing.T) {
868868
}
869869

870870
func TestThresholdedResultInt64(t *testing.T) {
871-
// ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise)
871+
// ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise)
872872
bs1 := getNoiselessBSI()
873873
bs1.Add(1)
874874
bs1.Add(2)
875875
bs1.Add(3)
876876
bs1.Add(4)
877-
got := bs1.ThresholdedResult(5)
877+
got := bs1.ThresholdedResult(0.1)
878878
if got == nil || *got != 10 {
879-
t.Errorf("ThresholdedResult(5): when 1, 2, 3, 4 were added got %v, want 10", got)
879+
t.Errorf("ThresholdedResult(0.1): when 1, 2, 3, 4 were added got %v, want 10", got)
880880
}
881881

882882
// ThresholdedResult outputs nil when it is less than the threshold
883883
bs2 := getNoiselessBSI()
884884
bs2.Add(1)
885885
bs2.Add(2)
886-
got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold()
886+
got = bs2.ThresholdedResult(0.1)
887+
if got != nil {
888+
t.Errorf("ThresholdedResult(0.1): when 1,2 were added got %v, want nil", got)
889+
}
890+
891+
// Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil.
892+
bs3 := getNoiselessBSI()
893+
bs3.Add(2)
894+
bs3.Add(3)
895+
got = bs3.ThresholdedResult(0.1)
887896
if got != nil {
888-
t.Errorf("ThresholdedResult(5): when 1,2 were added got %v, want nil", got)
897+
t.Errorf("ThresholdedResult(0.1): when 2,3 were added got %v, want nil", got)
889898
}
890899
}
891900

892901
func TestThresholdedResultFloat64(t *testing.T) {
893-
// ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise)
902+
// ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise)
894903
bs1 := getNoiselessBSF()
895904
bs1.Add(1.5)
896905
bs1.Add(2.5)
@@ -905,9 +914,9 @@ func TestThresholdedResultFloat64(t *testing.T) {
905914
bs2 := getNoiselessBSF()
906915
bs2.Add(1)
907916
bs2.Add(2.5)
908-
got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold()
917+
got = bs2.ThresholdedResult(0.1)
909918
if got != nil {
910-
t.Errorf("ThresholdedResult(5): when 1, 2.5 were added got %v, want nil", got)
919+
t.Errorf("ThresholdedResult(0.1): when 1, 2.5 were added got %v, want nil", got)
911920
}
912921
}
913922

0 commit comments

Comments
 (0)