From 6e74aef9e4c5ec3d55812554c194abe432275bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mira=C3=A7=20Vuslat=20Ba=C5=9Faran?= Date: Mon, 7 Feb 2022 15:39:28 +0100 Subject: [PATCH] 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. --- go/dpagg/count.go | 13 +++++++++---- go/dpagg/count_test.go | 16 +++++++++++++--- go/dpagg/dpagg_test.go | 2 +- go/dpagg/sum.go | 16 ++++++++++------ go/dpagg/sum_test.go | 25 +++++++++++++++++-------- 5 files changed, 50 insertions(+), 22 deletions(-) diff --git a/go/dpagg/count.go b/go/dpagg/count.go index 283de654..5e31dc0a 100644 --- a/go/dpagg/count.go +++ b/go/dpagg/count.go @@ -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 diff --git a/go/dpagg/count_test.go b/go/dpagg/count_test.go index ca081d6b..0303663a 100644 --- a/go/dpagg/count_test.go +++ b/go/dpagg/count_test.go @@ -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 @@ -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) } } diff --git a/go/dpagg/dpagg_test.go b/go/dpagg/dpagg_test.go index 9d76b117..f058e448 100644 --- a/go/dpagg/dpagg_test.go +++ b/go/dpagg/dpagg_test.go @@ -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]. diff --git a/go/dpagg/sum.go b/go/dpagg/sum.go index bec62b28..e8aee514 100644 --- a/go/dpagg/sum.go +++ b/go/dpagg/sum.go @@ -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 diff --git a/go/dpagg/sum_test.go b/go/dpagg/sum_test.go index df76f650..8c742af5 100644 --- a/go/dpagg/sum_test.go +++ b/go/dpagg/sum_test.go @@ -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) @@ -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) } }