Skip to content

Commit f637fda

Browse files
committed
Auto merge of #24370 - pnkfelix:fsk-unary-panic, r=<try>
Ensure a sole string-literal passed to `panic!` is not a fmt string. To accomplish this, adds `ensure_not_fmt_string_literal!` macro that will fail the compile if its expression argument is a fmt string literal. Since this is making a certain kind of use of `panic!` illegal, it is a: [breaking-change] In particular, a panic like this: ```rust panic!("Is it stringified code: { or is it a ill-formed fmt arg? }"); ``` must be rewritten; one easy rewrite is to add parentheses: ```rust panic!(("Is it stringified code: { or is it a ill-formed fmt arg? }")); ``` ---- Fix #22932.
2 parents af1c39c + 12706c9 commit f637fda

File tree

11 files changed

+308
-17
lines changed

11 files changed

+308
-17
lines changed

configure

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,35 @@ if [ -n "$CFG_ENABLE_DEBUG" ]; then
655655
CFG_ENABLE_DEBUG_JEMALLOC=1
656656
fi
657657

658+
at_most_one_opt() {
659+
local ARG=$1
660+
OP=$(echo $ARG | tr 'a-z-' 'A-Z_')
661+
DISABLE_NAME=\$CFG_DISABLE_${OP}
662+
ENABLE_NAME=\$CFG_ENABLE_${OP}
663+
eval VD=$DISABLE_NAME
664+
eval VE=$ENABLE_NAME
665+
if [ -n "${VD}" ] && [ -n "${VE}" ]; then
666+
msg "configure script bug: at most one of"
667+
msg "$DISABLE_NAME and $ENABLE_NAME may be set;"
668+
msg "you should report this as a bug, supplying your"
669+
msg "CFG_CONFIGURE_ARGS=$CFG_CONFIGURE_ARGS"
670+
err "configure script bug, at_most_one_opt $ARG"
671+
fi
672+
}
673+
674+
# At most one of each of the following enable/disable option pairs
675+
# should be set. Check that. (i.e. if we are going to have some
676+
# prioritization scheme, e.g. based on order of inputs to configure,
677+
# then such prioritization should be explicitly applied before this
678+
# point.)
679+
at_most_one_opt optimize
680+
at_most_one_opt optimize-cxx
681+
at_most_one_opt optimize-llvm
682+
at_most_one_opt llvm-assertions
683+
at_most_one_opt debug-assertions
684+
at_most_one_opt debuginfo
685+
at_most_one_opt debug-jemalloc
686+
658687
# OK, now write the debugging options
659688
if [ -n "$CFG_DISABLE_OPTIMIZE" ]; then putvar CFG_DISABLE_OPTIMIZE; fi
660689
if [ -n "$CFG_DISABLE_OPTIMIZE_CXX" ]; then putvar CFG_DISABLE_OPTIMIZE_CXX; fi

src/libcollections/fmt.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,3 +446,6 @@ pub fn format(args: Arguments) -> string::String {
446446
let _ = write!(&mut output, "{}", args);
447447
output
448448
}
449+
450+
#[unstable(feature = "ensure_not_fmt_string_literal")]
451+
pub use ::core::fmt::ENSURE_NOT_FMT_STRING_LITERAL_IS_UNSTABLE;

src/libcollections/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#![feature(slice_patterns)]
4242
#![feature(debug_builders)]
4343
#![feature(utf8_error)]
44+
#![feature(ensure_not_fmt_string_literal)] // (referenced in expansions)
4445
#![cfg_attr(test, feature(rand, rustc_private, test, hash, collections))]
4546
#![cfg_attr(test, allow(deprecated))] // rand
4647

src/libcore/fmt/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,5 +1084,16 @@ impl<'b, T: Debug> Debug for RefMut<'b, T> {
10841084
}
10851085
}
10861086

1087+
/// This constant is a trick to force `ensure_not_fmt_string_literal!`
1088+
/// to be treated as unstable whenever it occurs outside a macro
1089+
/// marked with `#[allow_internal_unstable]`.
1090+
///
1091+
/// This constant really should not ever be stabilized; if we ever
1092+
/// decide to stabilize the `ensure_not_fmt_string_literal!` macro
1093+
/// itself, then we should remove its use of this constant (and then
1094+
/// remove this constant).
1095+
#[unstable(feature = "ensure_not_fmt_string_literal")]
1096+
pub const ENSURE_NOT_FMT_STRING_LITERAL_IS_UNSTABLE: () = ();
1097+
10871098
// If you expected tests to be here, look instead at the run-pass/ifmt.rs test,
10881099
// it's a lot easier than creating all of the rt::Piece structures here.

src/libcore/macros.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,23 @@
88
// option. This file may not be copied, modified, or distributed
99
// except according to those terms.
1010

11+
// SNAP 5520801
12+
#[cfg(stage0)]
13+
#[macro_export]
14+
macro_rules! __unstable_rustc_ensure_not_fmt_string_literal {
15+
($name:expr, $e:expr) => { ((), $e) }
16+
}
17+
1118
/// Entry point of task panic, for details, see std::macros
1219
#[macro_export]
1320
macro_rules! panic {
1421
() => (
1522
panic!("explicit panic")
1623
);
1724
($msg:expr) => ({
18-
static _MSG_FILE_LINE: (&'static str, &'static str, u32) = ($msg, file!(), line!());
25+
static _MSG_FILE_LINE: (&'static str, &'static str, u32) =
26+
(__unstable_rustc_ensure_not_fmt_string_literal!("unary `panic!`", $msg).1,
27+
file!(), line!());
1928
::core::panicking::panic(&_MSG_FILE_LINE)
2029
});
2130
($fmt:expr, $($arg:tt)*) => ({
@@ -56,7 +65,8 @@ macro_rules! panic {
5665
macro_rules! assert {
5766
($cond:expr) => (
5867
if !$cond {
59-
panic!(concat!("assertion failed: ", stringify!($cond)))
68+
const MSG: &'static str = concat!("assertion failed: ", stringify!($cond));
69+
panic!(MSG)
6070
}
6171
);
6272
($cond:expr, $($arg:tt)+) => (

src/libcoretest/nonzero.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,6 @@ fn test_match_option_string() {
9595
let five = "Five".to_string();
9696
match Some(five) {
9797
Some(s) => assert_eq!(s, "Five"),
98-
None => panic!("unexpected None while matching on Some(String { ... })")
98+
None => panic!("{}", "unexpected None while matching on Some(String { ... })")
9999
}
100100
}

src/libstd/macros.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
1717
#![unstable(feature = "std_misc")]
1818

19+
// SNAP 5520801
20+
#[cfg(stage0)]
21+
#[macro_export]
22+
macro_rules! __unstable_rustc_ensure_not_fmt_string_literal {
23+
($name:expr, $e:expr) => { ((), $e) }
24+
}
25+
1926
/// The entry point for panic of Rust tasks.
2027
///
2128
/// This macro is used to inject panic into a Rust task, causing the task to
@@ -44,7 +51,8 @@ macro_rules! panic {
4451
panic!("explicit panic")
4552
});
4653
($msg:expr) => ({
47-
$crate::rt::begin_unwind($msg, {
54+
$crate::rt::begin_unwind(
55+
__unstable_rustc_ensure_not_fmt_string_literal!("unary `panic!`", $msg).1, {
4856
// static requires less code at runtime, more constant data
4957
static _FILE_LINE: (&'static str, usize) = (file!(), line!() as usize);
5058
&_FILE_LINE
@@ -90,11 +98,12 @@ macro_rules! panic {
9098
panic!("explicit panic")
9199
});
92100
($msg:expr) => ({
93-
$crate::rt::begin_unwind($msg, {
94-
// static requires less code at runtime, more constant data
95-
static _FILE_LINE: (&'static str, u32) = (file!(), line!());
96-
&_FILE_LINE
97-
})
101+
$crate::rt::begin_unwind(
102+
__unstable_rustc_ensure_not_fmt_string_literal!("unary `panic!`", $msg), {
103+
// static requires less code at runtime, more constant data
104+
static _FILE_LINE: (&'static str, u32) = (file!(), line!());
105+
&_FILE_LINE
106+
})
98107
});
99108
($fmt:expr, $($arg:tt)+) => ({
100109
$crate::rt::begin_unwind_fmt(format_args!($fmt, $($arg)+), {

src/libsyntax/ext/base.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,9 @@ fn initial_syntax_expander_table<'feat>(ecfg: &expand::ExpansionConfig<'feat>)
458458
syntax_expanders.insert(intern("format_args"),
459459
// format_args uses `unstable` things internally.
460460
NormalTT(Box::new(ext::format::expand_format_args), None, true));
461+
syntax_expanders.insert(intern("__unstable_rustc_ensure_not_fmt_string_literal"),
462+
builtin_normal_expander(
463+
ext::format::ensure_not_fmt_string_literal));
461464
syntax_expanders.insert(intern("env"),
462465
builtin_normal_expander(
463466
ext::env::expand_env));
@@ -748,19 +751,38 @@ impl<'a> ExtCtxt<'a> {
748751
}
749752
}
750753

754+
pub type ExprStringLitResult =
755+
Result<(P<ast::Expr>, InternedString, ast::StrStyle), P<ast::Expr>>;
756+
757+
/// Extract a string literal from macro expanded version of `expr`.
758+
///
759+
/// if `expr` is not string literal, then Err with span for expanded
760+
/// input, but does not emit any messages nor stop compilation.
761+
pub fn expr_string_lit(cx: &mut ExtCtxt, expr: P<ast::Expr>) -> ExprStringLitResult
762+
{
763+
// we want to be able to handle e.g. concat("foo", "bar")
764+
let expr = cx.expander().fold_expr(expr);
765+
let lit = match expr.node {
766+
ast::ExprLit(ref l) => match l.node {
767+
ast::LitStr(ref s, style) => Some(((*s).clone(), style)),
768+
_ => None
769+
},
770+
_ => None
771+
};
772+
match lit {
773+
Some(lit) => Ok((expr, lit.0, lit.1)),
774+
None => Err(expr),
775+
}
776+
}
777+
751778
/// Extract a string literal from the macro expanded version of `expr`,
752779
/// emitting `err_msg` if `expr` is not a string literal. This does not stop
753780
/// compilation on error, merely emits a non-fatal error and returns None.
754781
pub fn expr_to_string(cx: &mut ExtCtxt, expr: P<ast::Expr>, err_msg: &str)
755782
-> Option<(InternedString, ast::StrStyle)> {
756-
// we want to be able to handle e.g. concat("foo", "bar")
757-
let expr = cx.expander().fold_expr(expr);
758-
match expr.node {
759-
ast::ExprLit(ref l) => match l.node {
760-
ast::LitStr(ref s, style) => return Some(((*s).clone(), style)),
761-
_ => cx.span_err(l.span, err_msg)
762-
},
763-
_ => cx.span_err(expr.span, err_msg)
783+
match expr_string_lit(cx, expr) {
784+
Ok((_, s, style)) => return Some((s, style)),
785+
Err(e) => cx.span_err(e.span, err_msg),
764786
}
765787
None
766788
}

src/libsyntax/ext/format.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,111 @@ impl<'a, 'b> Context<'a, 'b> {
632632
}
633633
}
634634

635+
/// Expands `ensure_not_fmt_string_literal!(<where-literal>, <expr>)`
636+
/// into `<expr>`, but ensures that if `<expr>` is a string-literal,
637+
/// then it is not a potential format string literal.
638+
pub fn ensure_not_fmt_string_literal<'cx>(cx: &'cx mut ExtCtxt,
639+
sp: Span,
640+
tts: &[ast::TokenTree])
641+
-> Box<base::MacResult+'cx> {
642+
use fold::Folder;
643+
let takes_two_args = |cx: &ExtCtxt, rest| {
644+
cx.span_err(sp, &format!("`ensure_not_fmt_string_literal!` \
645+
takes 2 arguments, {}", rest));
646+
DummyResult::expr(sp)
647+
};
648+
let mut p = cx.new_parser_from_tts(tts);
649+
if p.token == token::Eof { return takes_two_args(cx, "given 0"); }
650+
let arg1 = cx.expander().fold_expr(p.parse_expr());
651+
if p.token == token::Eof { return takes_two_args(cx, "given 1"); }
652+
if !panictry!(p.eat(&token::Comma)) { return takes_two_args(cx, "comma-separated"); }
653+
if p.token == token::Eof { return takes_two_args(cx, "given 1"); }
654+
let arg2 = cx.expander().fold_expr(p.parse_expr());
655+
if p.token != token::Eof {
656+
takes_two_args(cx, "given too many");
657+
// (but do not return; handle two provided, nonetheless)
658+
}
659+
660+
// First argument is name of where this was invoked (for error messages).
661+
let lit_str_with_extended_lifetime;
662+
let name: &str = match expr_string_lit(cx, arg1) {
663+
Ok((_, lit_str, _)) => {
664+
lit_str_with_extended_lifetime = lit_str;
665+
&lit_str_with_extended_lifetime
666+
}
667+
Err(expr) => {
668+
let msg = "first argument to `ensure_not_fmt_string_literal!` \
669+
must be string literal";
670+
cx.span_err(expr.span, msg);
671+
// Compile proceeds using "ensure_not_fmt_string_literal"
672+
// as the name.
673+
"ensure_not_fmt_string_literal!"
674+
}
675+
};
676+
677+
// Second argument is the expression we are checking.
678+
let warning = |cx: &ExtCtxt, c: char| {
679+
cx.span_warn(sp, &format!("{} literal argument contains `{}`", name, c));
680+
cx.span_note(sp, "Is it meant to be a `format!` string?");
681+
cx.span_help(sp, "You can wrap the argument in parentheses \
682+
to sidestep this warning");
683+
};
684+
685+
let expr = match expr_string_lit(cx, arg2) {
686+
Err(expr) => {
687+
// input was not a string literal; just ignore it.
688+
expr
689+
}
690+
691+
Ok((expr, lit_str, _style)) => {
692+
for c in lit_str.chars() {
693+
if c == '{' || c == '}' {
694+
warning(cx, c);
695+
break;
696+
}
697+
}
698+
// we still return the expr itself, to allow catching of
699+
// further errors in the input.
700+
expr
701+
}
702+
};
703+
704+
let unstable_marker = cx.expr_path(cx.path_global(sp, vec![
705+
cx.ident_of_std("core"),
706+
cx.ident_of("fmt"),
707+
cx.ident_of("ENSURE_NOT_FMT_STRING_LITERAL_IS_UNSTABLE")]));
708+
709+
// Total hack: We do not (yet) have hygienic-marking of stabilty.
710+
// Thus an unstable macro (like `ensure_not_fmt_string!`) can leak
711+
// through another macro (like `panic!`), where the latter is just
712+
// using the former as an implementation detail.
713+
//
714+
// The `#[allow_internal_unstable]` system does not suffice to
715+
// address this; it explicitly (as described on Issue #22899)
716+
// disallows the use of unstable functionality via a helper macro
717+
// like `ensure_not_fmt_string!`, by design.
718+
//
719+
// So, the hack: the `ensure_not_fmt_string!` macro has just one
720+
// stable client: `panic!`. So we give `panic!` a backdoor: we
721+
// allow its name literal string to give it stable access. Any
722+
// other argument that is passed in will cause us to emit the
723+
// unstable-marker, which will then be checked against the enabled
724+
// feature-set.
725+
//
726+
// This, combined with the awkward actual name of the unstable
727+
// macro (hint: the real name is far more awkward than the one
728+
// given in this comment) should suffice to ensure that people do
729+
// not accidentally commit to using it.
730+
let marker = if name == "unary `panic!`" {
731+
cx.expr_tuple(sp, vec![]) // i.e. `()`
732+
} else {
733+
unstable_marker
734+
};
735+
let expr = cx.expr_tuple(sp, vec![marker, expr]);
736+
737+
MacEager::expr(expr)
738+
}
739+
635740
pub fn expand_format_args<'cx>(ecx: &'cx mut ExtCtxt, sp: Span,
636741
tts: &[ast::TokenTree])
637742
-> Box<base::MacResult+'cx> {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
// This test is ensuring that the `ensure_not_fmt_string_literal!`
12+
// macro cannot be invoked (at least not in code whose expansion ends
13+
// up in the expanded output) without the appropriate feature.
14+
15+
pub fn f0() {
16+
panic!("this should work");
17+
}
18+
19+
pub fn main() {
20+
__unstable_rustc_ensure_not_fmt_string_literal!(
21+
"`main`", "this should work, but its unstable");
22+
//~^^ ERROR use of unstable library feature 'ensure_not_fmt_string_literal'
23+
//~| HELP add #![feature(ensure_not_fmt_string_literal)] to the crate attributes to enable
24+
f0();
25+
}

0 commit comments

Comments
 (0)