Skip to content

Commit ebd417a

Browse files
authored
Merge branch 'main' into fix/25941-preserve-timezone-cast
2 parents 10c9fb3 + b40d696 commit ebd417a

31 files changed

Lines changed: 1727 additions & 64 deletions

File tree

‎SECURITY.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<!---
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
20+
# Security Policy
21+
22+
This document outlines the security model for Apache DataFusion and how to
23+
report vulnerabilities.
24+
25+
This model also applies to the [datafusion-cli] command line tool, which is
26+
a thin wrapper around the DataFusion library.
27+
28+
## Security Model
29+
30+
DataFusion is a low level library, designed to be embedded in applications
31+
that have their own security model. This section describes DataFusion's own
32+
model: what counts as a bug versus a vulnerability.
33+
34+
In general, crashes, panics, hangs, and excessive resource consumption
35+
(memory, CPU, or disk) are treated as **bugs**, not vulnerabilities, unless
36+
they are **exploitable** and could let an attacker:
37+
38+
* Execute arbitrary code (Remote Code Execution), or
39+
* Exfiltrate sensitive information from process memory (Information Disclosure).
40+
41+
If the exploitation path is unclear, please report the issue as a bug rather
42+
than a vulnerability. The sections below describe what DataFusion treats as
43+
trusted input in a few specific areas.
44+
45+
### SQL and DataFrame Queries
46+
47+
SQL and DataFrame queries are executable code, comparable to a scripting
48+
language: a query can legitimately read files, open network connections
49+
(e.g. via `CREATE EXTERNAL TABLE`), and consume significant CPU, memory, or
50+
disk. Running such a query is not, on its own, a vulnerability.
51+
52+
It is the embedding application's responsibility to decide whether a query is
53+
safe to run (e.g. validating externally supplied SQL text or URLs) and to
54+
sandbox untrusted queries at the OS/container level if needed. APIs such as
55+
[`SQLOptions::with_allow_dml`] can help restrict what a query is allowed to
56+
do, but do not guarantee that all input is safe to execute.
57+
58+
### Data Files
59+
60+
Format readers (e.g. Parquet, CSV, JSON, Avro, and Arrow IPC) assume a
61+
well-formed file from a trusted writer; use the [arrow validation APIs] to
62+
validate an untrusted file's structure. Issues due to malformed files, and
63+
well-formed files that contain unexpected or adversarial *data*, are bugs rather
64+
than vulnerabilities, as explained above.
65+
66+
### Serialized Plans
67+
68+
Serialized plans, such as [Substrait] and [`datafusion-proto`], are treated
69+
as trusted input. If received from an untrusted source, they should be
70+
validated before being passed to DataFusion for execution.
71+
72+
### Extensions and Other Uses of Public APIs
73+
74+
Code that uses DataFusion's public extension APIs (e.g. user defined functions,
75+
`TableProvider`, `ExecutionPlan`) is trusted to uphold their contracts (for
76+
example, that any `ArrayRef` is a valid Arrow array). Issues arising from
77+
violating the API contracts are not considered a DataFusion vulnerability.
78+
79+
## Reporting a Bug
80+
81+
We treat all bugs seriously and welcome help fixing them. If you find a bug
82+
that does not meet the criteria for a security vulnerability, please report it
83+
in the [public issue tracker](https://github.com/apache/datafusion/issues/).
84+
85+
## Reporting a Vulnerability
86+
87+
For security vulnerabilities **do not file a public issue.** Follow the [ASF
88+
security reporting process] by emailing
89+
[security@apache.org](mailto:security@apache.org).
90+
91+
Include in your report:
92+
93+
- A clear description and minimal reproducer.
94+
- Affected crates and versions.
95+
- Potential impact.
96+
97+
[datafusion-cli]: https://datafusion.apache.org/user-guide/cli/index.html
98+
[`sqloptions::with_allow_dml`]: https://docs.rs/datafusion/latest/datafusion/execution/context/struct.SQLOptions.html#method.with_allow_dml
99+
[arrow validation apis]: https://docs.rs/arrow/latest/arrow/array/struct.ArrayData.html#method.validate_full
100+
[substrait]: https://docs.rs/datafusion-substrait/latest/datafusion_substrait/
101+
[`datafusion-proto`]: https://docs.rs/datafusion-proto/latest/datafusion_proto/
102+
[asf security reporting process]: https://www.apache.org/security/#reporting-a-vulnerability

‎datafusion/common/src/config.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,17 @@ config_namespace! {
14261426
/// Defaults to 20.
14271427
pub max_in_list_size: usize, default = 20
14281428

1429+
/// (reading) If true, top-level string and binary Parquet columns with
1430+
/// dictionary pages are inferred and scanned as
1431+
/// `Dictionary<Int32, Utf8>` / `Dictionary<Int32, Binary>` instead of
1432+
/// their plain value type.
1433+
///
1434+
/// This applies only when DataFusion infers the table schema. Tables with
1435+
/// a user-supplied schema are not promoted because the Parquet footer is
1436+
/// not read at DDL time, so dictionary pages cannot be detected per column.
1437+
/// See <https://github.com/apache/datafusion/issues/24112>
1438+
pub enable_rle_to_dictionary: bool, default = false
1439+
14291440
/// (reading) Which byte range of a split file reads each row group.
14301441
/// `start_offset` picks the range containing the row group's start.
14311442
/// `midpoint` picks the range containing its midpoint, as Spark does,

‎datafusion/common/src/file_options/parquet_writer.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ impl ParquetOptions {
249249
skip_arrow_metadata: _,
250250
max_predicate_cache_size: _,
251251
max_in_list_size: _,
252+
enable_rle_to_dictionary: _,
252253
row_group_range_assignment: _,
253254
} = self;
254255

@@ -429,6 +430,7 @@ mod tests {
429430
coerce_int96_tz: None,
430431
max_predicate_cache_size: defaults.max_predicate_cache_size,
431432
content_defined_chunking: defaults.content_defined_chunking.clone(),
433+
enable_rle_to_dictionary: defaults.enable_rle_to_dictionary,
432434
}
433435
}
434436

@@ -558,6 +560,8 @@ mod tests {
558560
coerce_int96: None,
559561
coerce_int96_tz: None,
560562
content_defined_chunking: props.content_defined_chunking().into(),
563+
enable_rle_to_dictionary: global_options_defaults
564+
.enable_rle_to_dictionary,
561565
},
562566
column_specific_options,
563567
key_value_metadata,

‎datafusion/datasource-parquet/src/file_format.rs‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,16 @@ impl FileFormat for ParquetFormat {
356356
&object.location,
357357
)
358358
.await?;
359-
let result = DFParquetMetadata::new(store.as_ref(), object)
359+
let meta = DFParquetMetadata::new(store.as_ref(), object)
360360
.with_metadata_size_hint(self.metadata_size_hint())
361361
.with_decryption_properties(file_decryption_properties)
362362
.with_file_metadata_cache(Some(Arc::clone(&file_metadata_cache)))
363363
.with_coerce_int96(coerce_int96)
364364
.with_coerce_int96_tz(coerce_int96_tz.clone())
365-
.fetch_schema_with_location()
366-
.await?;
365+
.with_enable_rle_to_dictionary(
366+
self.options.global.enable_rle_to_dictionary,
367+
);
368+
let result = meta.fetch_schema_with_location().await?;
367369
Ok::<_, DataFusionError>(result)
368370
})
369371
.boxed() // Workaround https://github.com/rust-lang/rust/issues/64552
@@ -398,7 +400,12 @@ impl FileFormat for ParquetFormat {
398400
}
399401
drop(seen);
400402

401-
let schemas = schemas.into_iter().map(|(_, schema)| schema);
403+
// Normalize dict-promoted schemas before merging so mixed dict/plain files merge cleanly.
404+
let mut schemas: Vec<Schema> =
405+
schemas.into_iter().map(|(_, schema)| schema).collect();
406+
if self.options.global.enable_rle_to_dictionary {
407+
schemas = crate::schema_coercion::uniform_dict_schemas(schemas);
408+
}
402409

403410
let schema = if self.skip_metadata() {
404411
Schema::try_merge(clear_metadata(schemas))
@@ -520,7 +527,7 @@ impl FileFormat for ParquetFormat {
520527
source = source.with_parquet_file_reader_factory(cached_parquet_read_factory);
521528

522529
if let Some(metadata_size_hint) = metadata_size_hint {
523-
source = source.with_metadata_size_hint(metadata_size_hint)
530+
source = source.with_metadata_size_hint(metadata_size_hint);
524531
}
525532

526533
source = self.set_source_encryption_factory(source, state)?;
@@ -672,6 +679,7 @@ impl From<&ParquetFormatFactory> for protobuf::TableParquetOptions {
672679
compression_opt: global_options.global.compression.map(|compression| {
673680
parquet_options::CompressionOpt::Compression(compression.to_string())
674681
}),
682+
enable_rle_to_dictionary: global_options.global.enable_rle_to_dictionary,
675683
dictionary_enabled_opt: global_options.global.dictionary_enabled.map(|enabled| {
676684
parquet_options::DictionaryEnabledOpt::DictionaryEnabled(enabled)
677685
}),

‎datafusion/datasource-parquet/src/metadata.rs‎

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ use crate::{Int96Coercer, apply_file_schema_type_coercions};
2323
use arrow::array::{Array, ArrayRef, BooleanArray};
2424
use arrow::compute::kernels::cmp::eq;
2525
use arrow::compute::{and, sum};
26-
use arrow::datatypes::{DataType, Schema, SchemaRef, TimeUnit};
26+
use arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
2727
use datafusion_common::encryption::FileDecryptionProperties;
2828
use datafusion_common::stats::Precision;
2929
use datafusion_common::{
30-
ColumnStatistics, DataFusionError, HashMap, Result, ScalarValue, Statistics,
30+
ColumnStatistics, DataFusionError, HashMap, HashSet, Result, ScalarValue, Statistics,
3131
internal_datafusion_err,
3232
};
3333
use datafusion_execution::cache::cache_manager::{
@@ -152,6 +152,8 @@ pub struct DFParquetMetadata<'a> {
152152
pub coerce_int96: Option<TimeUnit>,
153153
/// Optional timezone applied to INT96-coerced timestamps.
154154
pub coerce_int96_tz: Option<Arc<str>>,
155+
/// If true, promote string/binary columns with dictionary pages to `Dictionary(Int32, ...)`.
156+
enable_rle_to_dictionary: bool,
155157
}
156158

157159
impl<'a> DFParquetMetadata<'a> {
@@ -169,9 +171,16 @@ impl<'a> DFParquetMetadata<'a> {
169171
page_index_policy: None,
170172
coerce_int96: None,
171173
coerce_int96_tz: None,
174+
enable_rle_to_dictionary: false,
172175
}
173176
}
174177

178+
/// Promote string/binary columns with dictionary pages to `Dictionary(Int32, ...)`.
179+
pub fn with_enable_rle_to_dictionary(mut self, enable: bool) -> Self {
180+
self.enable_rle_to_dictionary = enable;
181+
self
182+
}
183+
175184
/// Set a hint for the number of trailing bytes to prefetch from the end
176185
/// of the file, equivalent to
177186
/// [`ParquetMetaDataReader::with_prefetch_hint`].
@@ -449,6 +458,68 @@ impl<'a> DFParquetMetadata<'a> {
449458
.coerce()
450459
})
451460
.unwrap_or(schema);
461+
462+
let schema = if self.enable_rle_to_dictionary {
463+
let schema_descr = file_metadata.schema_descr();
464+
// Top-level columns that have a dictionary page in at least one row group.
465+
let dict_cols: HashSet<String> = metadata
466+
.row_groups()
467+
.iter()
468+
.flat_map(|rg| {
469+
rg.columns()
470+
.iter()
471+
.enumerate()
472+
.filter_map(|(col_idx, col)| {
473+
col.dictionary_page_offset()?;
474+
let col_desc = schema_descr.column(col_idx);
475+
let parts = col_desc.path().parts();
476+
// Skip nested columns: their leaf name doesn't match the
477+
// Arrow top-level field name.
478+
(parts.len() == 1).then(|| parts[0].clone())
479+
})
480+
})
481+
.collect();
482+
if dict_cols.is_empty() {
483+
schema
484+
} else {
485+
let promoted: Vec<_> = schema
486+
.fields()
487+
.iter()
488+
.map(|field| {
489+
if !dict_cols.contains(field.name()) {
490+
return Arc::clone(field);
491+
}
492+
let dict_value_type = match field.data_type() {
493+
DataType::Utf8 => Some(DataType::Utf8),
494+
DataType::LargeUtf8 => Some(DataType::LargeUtf8),
495+
DataType::Binary => Some(DataType::Binary),
496+
DataType::LargeBinary => Some(DataType::LargeBinary),
497+
_ => None,
498+
};
499+
dict_value_type.map_or_else(
500+
|| Arc::clone(field),
501+
|value_type| {
502+
Arc::new(
503+
Field::new(
504+
field.name(),
505+
DataType::Dictionary(
506+
Box::new(DataType::Int32),
507+
Box::new(value_type),
508+
),
509+
field.is_nullable(),
510+
)
511+
.with_metadata(field.metadata().clone()),
512+
)
513+
},
514+
)
515+
})
516+
.collect();
517+
Schema::new_with_metadata(promoted, schema.metadata().clone())
518+
}
519+
} else {
520+
schema
521+
};
522+
452523
Ok(schema)
453524
}
454525

0 commit comments

Comments
 (0)