Skip to content

Instantly share code, notes, and snippets.

@tomrittervg
Last active January 12, 2020 01:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomrittervg/5ca35126db62d2a2f4fcddd1aef4b5eb to your computer and use it in GitHub Desktop.
Save tomrittervg/5ca35126db62d2a2f4fcddd1aef4b5eb to your computer and use it in GitHub Desktop.
Tainted IPC Ergonomics
The goal of this is to expose values that come from IPC as Tainted<int> instead of simply int.
To get the value, we force the developer to perform some validation first.
<Benefits elided>
#include <iostream>
#include "work.h"
using namespace std;
class Coord {
private:
int x, y;
public:
Coord(int a, int b)
: x(a), y(b) {}
bool Valid() {
return true;
}
};
int main() {
Tainted<int> x(50);
int y = MOZ_VALIDATE_AND_GET(x, x > 10);
cout<<"Untainted the value "<<y<<endl;
/* These should fail:
int yyy = x;
if (x < 10);
*/
int z1 = MOZ_VALIDATE_AND_GET(x, x < 10);
int z2 = MOZ_VALIDATE_AND_GET(x, x < 10, "Custom Assertion String");
int z3 = MOZ_VALIDATE_AND_GET(x, [&x] {
bool intermediate_result = x < 10;
if (intermediate_result) {
return true;
} else {
return x > 25;
}
}());
int w = MOZ_VALIDATE_OR(x, x > 100, 2);
cout<<"MOZ_VALIDATE_OR(x, x > 10, 2) = "<<w<<endl;
if(MOZ_IS_VALID(x, (x > 10))) {
cout<<"Tested validity of x (and it passed)"<<endl;
} else {
cout<<"Tested validity of x (and it FAILED. That's a bug.)"<<endl;
}
bool xIsValid = MOZ_IS_VALID(x, (x > 10));
cout << "Got validity of x in a bool: "<<xIsValid<<" (should be true)"<<endl;
// -----------------
Tainted<Coord> a(Coord(1, 2));
Coord b = MOZ_VALIDATE_AND_GET(a, a.Valid(), "fail");
if(MOZ_IS_VALID(a, (a.Valid())));
return 0;
}
// ================================================
// Ignore this section : I needed to copy/paste a crap from our tree
#define MOZ_CONCAT2(x, y) x##y
#define MOZ_CONCAT(x, y) MOZ_CONCAT2(x, y)
#define MOZ_ARG_COUNT(...) \
MOZ_MACROARGS_ARG_COUNT_HELPER2(MOZ_MACROARGS_ARG_COUNT_HELPER(__VA_ARGS__))
#define MOZ_MACROARGS_ARG_COUNT_HELPER(...) \
(_, ##__VA_ARGS__, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, \
36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, \
17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define MOZ_MACROARGS_ARG_COUNT_HELPER2(aArgs) \
MOZ_MACROARGS_ARG_COUNT_HELPER3 aArgs
#define MOZ_MACROARGS_ARG_COUNT_HELPER3( \
a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, \
a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, \
a32, a33, a34, a35, a36, a37, a38, a39, a40, a41, a42, a43, a44, a45, a46, \
a47, a48, a49, a50, a51, ...) \
a51
#define MOZ_PASTE_PREFIX_AND_ARG_COUNT_GLUE(a, b) a b
#define MOZ_PASTE_PREFIX_AND_ARG_COUNT(aPrefix, ...) \
MOZ_PASTE_PREFIX_AND_ARG_COUNT_GLUE(MOZ_CONCAT, \
(aPrefix, MOZ_ARG_COUNT(__VA_ARGS__)))
#define MOZ_ASSERT_GLUE(a, b) a b
#define MOZ_RELEASE_ASSERT(x, str) \
if(!x) { cout<<" FAKE ASSERTION: " #x " " <<str<<endl; }
// ================================================
/*
* Simple Tainted<foo> class
*
* Class should not support any de-reference or comparison operator and instead
* force all access to the member variable through the MOZ_VALIDATE macros.
*
* While the Coerce() function is publicly accessible on the class, it should
* only be used by the MOZ_VALIDATE macros, and static analysis will prevent
* it being used elsewhere.
*/
template<typename T>
class Tainted {
private:
T mValue;
public:
Tainted<T>(T aValue)
: mValue(aValue) {
}
inline T Coerce() {
return this->mValue;
}
};
// ================================================
/*
* Macros to validate and un-taint a value.
*
* All macros accept the tainted variable as the first argument, and a condition
* as the second argument. If the condition is satisfied, then the value is considered
* valid.
*
* Usage:
* Tainted<int> a(50);
*
*
* int w = MOZ_VALIDATE_AND_GET(a, a < 10);
* Here we check that a is greater than 10 and if so, we
* return its value. If it is not greater than 10 we MOZ_RELEASE_ASSERT.
*
* Note that while the comparison of a < 10 works inside the macro,
* doing so outside the macro (such as with if (a < 10) will (intentionally)
* fail. We do this to ensure that all validation logic is self-contained.
*
* Also note this does NOT support multi-variable initializations of the form
* int z = MOZ_VALIDATE_AND_GET(x, true), zz = MOZ_VALIDATE_AND_GET(x, true);
* Just put them on different lines.
*
*
* int x = MOZ_VALIDATE_AND_GET(a, a < 10, "a is too large");
* The macro also supports supplying a custom string to the MOZ_RELEASE_ASSERT
*
*
* int y = MOZ_VALIDATE_AND_GET(a, [&a] {
* bool intermediate_result = someFunction(a);
* if (intermediate_result) {
* return true;
* } else {
* return someOtherFunction(a);
* }
* }());
* In this example we use a lambda function to perform a more complicated
* validation that cannot be performed succinctly in a single conditional.
*
*
* // Value is safe to use because we only print it into a string
* int z = MOZ_VALIDATE_AND_GET(a, true);
* It is *possible* to specify a boolean 'true' to force the validation
* to always pass. However, static analysis will require the preceeding
* line to be a comment, and human decency requires the comment explain why
* the validation can be safely bypassed.
*
*
* int safeOffset = MOZ_VALIDATE_OR(a, a < 10, 0);
* MOZ_VALIDATE_OR will provide a default value to use if the tainted value
* is invalid.
*
*
*
* if (!MOZ_IS_VALID(a, a > 50)) {
* return false;
* }
* If you want to test validity without triggering an assertion, the MOZ_IS_VALID
* macro can be used anywhere as a boolean.
*
*/
// We use the same variable name in the nested scope, shadowing the outer
// scope - this allows the user to write the same variable name in the
// macro's condition without using a magic name like 'value'.
#define MOZ_VALIDATE_AND_GET_HELPER3(tainted_value, condition, assertionstring) \
tainted_value.Coerce(); \
do { \
auto tmp = tainted_value.Coerce(); \
auto tainted_value = tmp; \
MOZ_RELEASE_ASSERT((condition), assertionstring); \
} while(false);
// This and the next macros are heavy C++ gobblygook to support a two and three
// argument variant.
#define MOZ_VALIDATE_AND_GET_HELPER2(tainted_value, condition) \
MOZ_VALIDATE_AND_GET_HELPER3(tainted_value, condition, "MOZ_VALIDATE_AND_GET(" #tainted_value ", " #condition ") has failed")
#define MOZ_VALIDATE_AND_GET(...) \
MOZ_ASSERT_GLUE( \
MOZ_PASTE_PREFIX_AND_ARG_COUNT(MOZ_VALIDATE_AND_GET_HELPER, __VA_ARGS__), \
(__VA_ARGS__))
// This construct uses a lambda expression to create a scope and test the
// condition, returning true or false.
// We use the same variable-shadowing trick.
#define MOZ_IS_VALID(tainted_value, condition) \
[&tainted_value]() { \
auto tmp = tainted_value.Coerce(); \
auto tainted_value = tmp; \
return (condition); \
}()
// Allows us to test validity and returning a default value if the value is invalid.
#define MOZ_VALIDATE_OR(tainted_value, condition, alternate_value) \
(MOZ_IS_VALID(tainted_value, condition) ? tainted_value.Coerce() : alternate_value)
/*
TODO:
- Figure out if there are helpers that would be useful for Strings and Principals
- Write static analysis to enforce invariants:
- No use of .Coerce() except in the header file.
- If a constant is passed as the condition, require the preceeding line be a comment.
*/
@deian
Copy link

deian commented Jan 12, 2020

This looks pretty great! It's probably worth copying these macros into RLBox too, especially as we port more things (cleaner + nice to have close-enough API).

FWIW the MOZ_VALIDATE_OR was a bit confusing for me on first glance -- I thought it would be the 2nd condition (vs. default value), but I'm not sure it's worth bike-shedding this.

I think it might be useful to add a few things to the interface, but this can totally be done piecemeal.

  • Operators on Tainted values (e.g., && and +) that produce tainted values. The main reason for this is to avoid unwrapping values too early. It may also be easier to validate the result of computation on tainted values than individual pieces (e.g., image size should not exceed XYZ). Of course, because values are copied across the IPC boundary this might be less important. It's also something that can just be added later on a need basis.

  • Kind of similar to the first point, but Shravan is suggesting that it may be worth turning class members/struct field accesses to produce tainted values. This would let you verify parts of objects as they are used vs. whole objects. I think this one may be a bigger deal because if you forget to verify one field you won't know at use time.

    Looking at some of the IPC layer audits and ipdlh files: it seems like structs can have pointers. Are these treated in a special way? (E.g., are they checked or sanitized on copy?) If not, at the very least these should always be tainted.

  • I didn't get a chance to look at the shared memory stuff in the IPC layer, but shared memory was the thing that made a bunch of the RLBox things a bit more complicated (e.g., the tainted_volatile). Will try to think this though soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment