mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-15 10:29:11 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d593a8f99d | ||
|
|
8bde636f27 |
Generated
+1
@@ -2349,6 +2349,7 @@ dependencies = [
|
||||
"craft-offcrypto",
|
||||
"craft-opc",
|
||||
"craft-xls",
|
||||
"craft-xml",
|
||||
"gridcraft-core",
|
||||
"gridcraft-formula",
|
||||
"gridcraft-functions",
|
||||
|
||||
@@ -40,6 +40,7 @@ craft-opc = { git = "https://github.com/storytold/craft-libs", rev = "bb57d422d0
|
||||
# turn them into workbooks.
|
||||
craft-csv = { git = "https://github.com/storytold/craft-libs", rev = "bb57d422d0bc842cbde430955825553ab7b976f4", version = "0.1.0" }
|
||||
# Excel 97-2003 (.xls, BIFF8) workbooks; `crates/xlsx/src/xls.rs` maps them onto the model.
|
||||
craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "bb57d422d0bc842cbde430955825553ab7b976f4", version = "0.1.0" }
|
||||
craft-xls = { git = "https://github.com/storytold/craft-libs", rev = "bb57d422d0bc842cbde430955825553ab7b976f4", version = "0.1.0" }
|
||||
|
||||
# foundation
|
||||
|
||||
@@ -292,6 +292,60 @@ print("socket quiet")
|
||||
assert!(String::from_utf8_lossy(&o.stdout).contains("socket quiet"), "{out}");
|
||||
}
|
||||
|
||||
/// The same inherited socket, by both ioctls that name the process to notify, `FIOSETOWN` (0x8901)
|
||||
/// and `SIOCSPGRP` (0x8902), with the argument a real pointer. Named by value the kernel reads the
|
||||
/// number as an address and fails with `EFAULT` whether or not the filter refuses it, so that form
|
||||
/// shows only that a rule matched a request number; by pointer each call succeeds unconfined and is
|
||||
/// `EPERM` confined, which is the transition the page claims. Kept apart from the test above so a
|
||||
/// rule dropped for one request number fails for that request, not behind another. Linux only, as
|
||||
/// those rules are: macOS's profile has none, and its standard streams here are pipes.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn a_confined_program_cant_point_an_inherited_socket_at_another_process_by_pointer() {
|
||||
if !confines() {
|
||||
return;
|
||||
}
|
||||
let Some((python, prefix)) = system_python() else {
|
||||
return;
|
||||
};
|
||||
let code = r#"
|
||||
import fcntl, socket, struct, sys
|
||||
other = int(sys.argv[1])
|
||||
def refused(call, *args):
|
||||
try:
|
||||
call(*args)
|
||||
except PermissionError:
|
||||
return True
|
||||
except OSError as e:
|
||||
print("other error", e)
|
||||
return False
|
||||
# Read first, so a standard stream that is not a socket can't pass for one: a pipe has no socket name.
|
||||
# Detached, not dropped: a socket object closes the descriptor it wrapped when it is collected, and then
|
||||
# the refusals below would be read off an invalid descriptor.
|
||||
wrapped = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, fileno=0)
|
||||
name = wrapped.getsockname()
|
||||
wrapped.detach()
|
||||
assert name == ""
|
||||
# The process group is a pointer, as a C caller passes it; naming it by value has the kernel read the
|
||||
# number as an address and fail with EFAULT, which would leave the refusal unexplained by the filter.
|
||||
group = bytearray(struct.pack("i", other))
|
||||
assert refused(fcntl.ioctl, 0, 0x8901, group), "giving a socket's notifications to another process"
|
||||
assert refused(fcntl.ioctl, 0, 0x8902, group), "giving a socket's notifications to another process's group"
|
||||
print("socket owner quiet")
|
||||
"#;
|
||||
let (mine, _) = std::os::unix::net::UnixStream::pair().unwrap();
|
||||
let socket: std::os::fd::OwnedFd = mine.into();
|
||||
let me = std::process::id().to_string();
|
||||
let o = Command::new(env!("CARGO_BIN_EXE_gridcraft-cli"))
|
||||
.args(["sandbox-check", "--read", &prefix, "--", &python, "-I", "-c", code, &me])
|
||||
.stdin(std::process::Stdio::from(socket))
|
||||
.output()
|
||||
.unwrap();
|
||||
let out = text(&o);
|
||||
assert!(o.status.success(), "{out}");
|
||||
assert!(String::from_utf8_lossy(&o.stdout).contains("socket owner quiet"), "{out}");
|
||||
}
|
||||
|
||||
/// Confined, a program changes no file, whether the policy lets it read that file or not, by path or
|
||||
/// by a descriptor: not its contents, permissions, owner, extended attributes, times or flags.
|
||||
/// Skipped when there is no `python3`.
|
||||
|
||||
@@ -23,6 +23,7 @@ craft-csv = { workspace = true }
|
||||
craft-offcrypto = { workspace = true }
|
||||
craft-opc = { workspace = true }
|
||||
craft-xls = { workspace = true }
|
||||
craft-xml = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
+19
-48
@@ -359,31 +359,9 @@ pub fn parse_streaming(xml: &[u8], split: &[&str], cb: &mut dyn FnMut(El) -> Res
|
||||
|
||||
// ---------------------------------------------------------------- writing helpers
|
||||
|
||||
/// Escapes text content.
|
||||
pub fn esc(s: &str) -> String {
|
||||
let mut o = String::with_capacity(s.len());
|
||||
push_esc(&mut o, s);
|
||||
o
|
||||
}
|
||||
|
||||
/// Appends [`esc`]`(s)` to `o`.
|
||||
pub fn push_esc(o: &mut String, s: &str) {
|
||||
if s.bytes().all(|b| matches!(b, b'\t' | b'\n' | 0x20..=0x7E) && !matches!(b, b'&' | b'<' | b'>')) {
|
||||
o.push_str(s);
|
||||
return;
|
||||
}
|
||||
o.reserve(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => o.push_str("&"),
|
||||
'<' => o.push_str("<"),
|
||||
'>' => o.push_str(">"),
|
||||
'\r' => o.push_str(" "),
|
||||
c if is_xml_char(c) => o.push(c),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// XML 1.0 byte escaping is shared; spreadsheet string and phonetic policy stays here.
|
||||
use craft_xml::is_xml_char;
|
||||
pub use craft_xml::{esc, esc_attr, push_esc};
|
||||
|
||||
/// Appends `n` in decimal.
|
||||
pub fn push_u64(o: &mut String, mut n: u64) {
|
||||
@@ -422,29 +400,6 @@ pub fn push_a1(o: &mut String, c: gridcraft_core::CellRef) {
|
||||
push_u64(o, u64::from(c.row) + 1);
|
||||
}
|
||||
|
||||
/// Escapes an attribute value (double-quoted).
|
||||
pub fn esc_attr(s: &str) -> String {
|
||||
let mut o = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => o.push_str("&"),
|
||||
'<' => o.push_str("<"),
|
||||
'>' => o.push_str(">"),
|
||||
'"' => o.push_str("""),
|
||||
'\n' => o.push_str(" "),
|
||||
'\r' => o.push_str(" "),
|
||||
'\t' => o.push_str("	"),
|
||||
c if is_xml_char(c) => o.push(c),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
fn is_xml_char(c: char) -> bool {
|
||||
matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}')
|
||||
}
|
||||
|
||||
/// ST_Xstring encoding for cell text: characters XML can't carry and carriage returns become
|
||||
/// `_xHHHH_`, and literal `_xHHHH_` sequences are protected with `_x005F_`. The result still needs
|
||||
/// [`esc`].
|
||||
@@ -805,3 +760,19 @@ mod tests {
|
||||
assert_eq!("1.5E20".parse::<f64>().unwrap(), 1.5e20);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod shared_escape_parity {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn xml_content_and_attributes_preserve_grid_bytes() {
|
||||
let value = "&<>\"'\t\n\r\0\u{1f}\u{fffe}\u{ffff}é😀";
|
||||
assert_eq!(esc(value), "&<>\"'\t\n é😀");
|
||||
assert_eq!(esc_attr(value), "&<>"'	 é😀");
|
||||
let mut appended = "prefix:".to_owned();
|
||||
push_esc(&mut appended, value);
|
||||
assert_eq!(appended, format!("prefix:{}", esc(value)));
|
||||
assert_eq!(esc("plain ASCII\t\n"), "plain ASCII\t\n");
|
||||
assert_eq!(decode_xstring(&encode_xstring("_x000D_\r\0😀")), "_x000D_\r\0😀");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user