2

Background (Intro)
Kint is a PHP debugging tool that works as a more powerful replacement to PHP's var_dump(), print_r(), and debug_backtrace(). One unusual -- for PHP at least -- feature of Kint is the ability to use real-time modifiers in the form of operands. Here is what the manual states about that feature:

There are a couple of real-time modifiers you can use:

  • ~d($var) this call will output in plain text format.
  • +d($var) will disregard depth level limits and output everything. Careful, this can hang your browser on large objects!
  • !d($var) will expand the output automatically.
  • -d($var) will attempt to ob_clean the previous output and flush after printing.
  • You can combine modifiers too: ~+d($var)

There is an older exiting SO question that is similar to this question if you need more information.


Questions

  1. How does Kint add these operands without triggering a PHP error?
  2. How can I emulate/capture any calls that use these operands?

Without loading Kint if you attempt to use these operands or create your own functions to capture Kint operand calls you get Fatal error: Uncaught Error: Unsupported operand types.

Important: I am using the kint.phar file and I'm not using composer or any kind of CLI usage.



My Use Case (Please don't get distracted from the Questions)
I'm adding this information for those that are curious and to further clarify my questions. I sincerely want to learn and understand HOW they are doing this and would appreciate answers to that end. This question is not about defending / critiquing / disagreeing with my use case:

For security and optimization I am creating a fake (empty) Kint class that loads when my site is in production mode. This ensures that any Kint calls left in the code on accident do not trigger fatal errors, can never print anything out, and uses less resources compared to loading the real Kint class.

I know you can disable Kint with Kint::$enabled_mode = false; but lets not focus on that. Here is the code I use to fake the Kint class. All that is missing is capturing calls that use these non-standard operands:

/**
 * Fake class.
 */
class Kint {

    const STATIC_BLACKHOLE = '';

    public static $enabled_mode = false;

    public function blackhole( $a ) {
        return;
    }

    public function __call( $m, $a ) {
        return call_user_func_array( array( $this, $this->blackhole ), $a );
    }

    public static function __callStatic( $m, $a ) {
        return self::STATIC_BLACKHOLE;
    }

}
$kint = new Kint();

// Alias of Kint::dump().

/**
 * Fake function to catch d().
 *
 * @return void
 */
function d() {
    return;
}

// Kint::dump basic mode.

/**
 * Fake function to catch s().
 *
 * @return void
 */
function s() {
    return;
}

\define( 'KINT_DIR', '/classes/Kint' );
\define( 'KINT_WIN', DIRECTORY_SEPARATOR !== '/' );
\define( 'KINT_PHP70', ( \version_compare( PHP_VERSION, '7.0' ) >= 0 ) );
\define( 'KINT_PHP71', ( \version_compare( PHP_VERSION, '7.1' ) >= 0 ) );
\define( 'KINT_PHP72', ( \version_compare( PHP_VERSION, '7.2' ) >= 0 ) );
\define( 'KINT_PHP73', ( \version_compare( PHP_VERSION, '7.3' ) >= 0 ) );
\define( 'KINT_PHP74', ( \version_compare( PHP_VERSION, '7.4' ) >= 0 ) );
\define( 'KINT_PHP80', ( \version_compare( PHP_VERSION, '8.0' ) >= 0 ) );
Blizzardengle
  • 992
  • 1
  • 17
  • 30
  • Attempting to replace all of Kint's possible runtime uses with noops is a rather inefficient and error-prone solution. If you use `--dev` on the composer require (as Kint recommends) and `--no-dev` on the deploy, then you won't possibly be able to run any Kint functions in production. If that's not sufficient to assuage your concerns, then add a check to your bootstrap and flat-out refuse to serve requests if you're in prod mode and Kint is available. If you're worried about accidentally leaving calls in your source code, use PHPCS to scan for forbidden functions before deploying. – Alex Howansky Nov 04 '21 at 20:14
  • 1
    @Alex - I really would like to learn how they managed to do this; I will remove the second half of my OP if needed. They did it some how and if nothing else but for curiosity I would appreciate understanding HOW. – Blizzardengle Nov 04 '21 at 20:25
  • The characters used as modifiers are already valid PHP unary operators, so it's not a syntax error to prefix function calls with them. Then (near as I can tell at a glance) when one of Kint's functions are called, it uses a debug backtrace to identify the source file, open it, locate the calling line, parse it to determine what modifiers were used, and react accordingly. It's somewhat mind-boggling that they didn't simply use function parameters for this. I'd never put this anywhere near my prod environments. – Alex Howansky Nov 04 '21 at 20:46
  • I agree, this is mind-boggling. With Kint running this works `~d($GLOBALS);` but without it you get `Fatal error: Uncaught Error: Unsupported operand types` because your not allowed to use operands like that in PHP. I'll try to look into the debug backtrace and see if I can make sense of it. – Blizzardengle Nov 04 '21 at 20:50
  • 1
    _"your not allowed to use operands like that in PHP"_ You absolutely are. `~d($GLOBALS)` is perfectly valid standard PHP syntax as long as `d()` returns something that the `~` operator works on. See https://3v4l.org/ReYos – Alex Howansky Nov 04 '21 at 20:58
  • +1 I had no clue you could do that. The info I had been reading said this shouldn't be done/possible. Thank you so much for pointing that out! Now it makes sense why they read the file, they need to see what data type to return to avoid triggering an error. – Blizzardengle Nov 04 '21 at 21:05
  • _"they need to see what data type to return"_ No, they can always just return an int and it'll work fine. What they need to see is which operator prefixes were used so that they can alter their output accordingly. I.e., normally, the fact that `d()` was prefixed with `~` is information that's not available to the code that's running _inside_ `d()`. – Alex Howansky Nov 04 '21 at 21:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/238874/discussion-between-blizzardengle-and-alex-howansky). – Blizzardengle Nov 04 '21 at 21:21

2 Answers2

2

The "real-time modifiers" are all valid PHP unary operators:

Thus, it is perfectly allowable to prefix function calls with these operators, as long as the function returns a type of value that the operator would normally work on:

function foo() {
    return 0;
}

// All of these work just fine, and generate no errors:
-foo();
+foo();
!foo();
~foo();

As far as I can tell at a glance, what Kint does inside its functions is use debug_backtrace() to get the source file where the function was called from. It then opens that file, reads it, locates the calling line, and parses it to determine what (if any) "real-time modifiers" were used to prefix the function call. I.e., consider the source:

function d($var) {
    // dump $var
}

~d($GLOBALS);

The code inside the d() function normally can not tell that its return value is about to be modified by the ~ operator. (And it shouldn't! Doing so completely violates lexical and logical scoping.) However, Kint escapes this scoping, re-parses the source file, finds the ~, and then uses that as a means to modify the output that the code inside the function generates.

This technique is very confusing, constitutes a huge performance hit, a scoping violating, and presents a security concern... all to provide an inferior implementation of a basic feature that the language already has -- function parameters. I would never let this module anywhere near any of my production servers.

That said, my recommendation would be to forget trying to override Kint's runtime functionality with noops. Instead build your pipeline so that Kint can not be deployed:

  • Make sure you use --dev in your compose require.
  • Make sure you use --no-dev in your deploy scripts.
  • Add a check to your bootstrap or front controller to immediately abort if Kint is loaded.
  • Use the "forbidden functions" sniff in PHPCS before deployment, to detect any usages of Kint functions that were left over in the source.
Alex Howansky
  • 50,515
  • 8
  • 78
  • 98
  • I 100% agree with your concerns and appreciate your recommendations. I haven't used PHP in a few years so I'm just getting back into it; I've been in the Node realm of things for a while. – Blizzardengle Nov 04 '21 at 22:57
  • Hey, original author of Kint here. I address the issues of "how in tarnation can this be possible" and "why in the world would anyone do this" in this answer: https://stackoverflow.com/a/69890023/179104 – raveren Nov 08 '21 at 21:21
  • "This technique is very confusing, constitutes a huge performance hit, a scoping violating, and presents a security concern..." that's hurtful and all **invalid** points besides the "confusing" part - which I address in my answer :) – raveren Nov 08 '21 at 21:56
0

Original author of Kint here.

I addressed most of the raised questions about implementation details of the operands themselves in this answer.

Now to the OP usecase:

What I would do usually is to include Kint in production! It's completely safe - or as safe as any other composer package. It is frequently updated for the past 10+ years. It's covered in tests and causes no measurable performance impact just by including it or even calling it when it's disabled.

Kint::$enabled_mode = false;

Furthermore, I love using it for logging complex variables (which ofc has a big performance cost to pay for verbosity), and similar functionality which needs it present in production anyway.

The operands, as stated in the linked answer, are shorthands for common use cases when realtime debugging. If you want to roll out permanent functionality which depends on the operands, I would just rewrite that to the verbose version to be way more readable:

So for example

$log = @d($var);

Becomes

$oldReturnValue = Kint::$return;
Kint::$return = true;

$log = Kint::dump($var);

Kint::$return = $oldReturnValue;
raveren
  • 17,799
  • 12
  • 70
  • 83
  • Thanks for posting! Is this explained anywhere in the GitHub repo? I think it would be helpful. I also agree about `Kint::$enabled_mode` but my OP was half out of curiosity -- could I actually noop KInt -- and just being security paranoid. I personally really appreciate what you've done with Kint. – Blizzardengle Nov 08 '21 at 22:07
  • thank you, that means a lot!!! :) It's not explained in the repo, no, the documentation is in dire need of love and attention, but I personally haven't had the time for years unfortunately :( – raveren Nov 08 '21 at 22:12