Abstract
Software verification tools that build machine-checked proofs of functional correctness usually focus on the algorithmic content of the code. Their proofs are not grounded in a formal semantic model of the environment that the program runs in, or the program’s interaction with that environment. As a result, several layers of translation and wrapper code must be trusted. In contrast, the CakeML project focuses on end-to-end verification to replace this trusted code with verified code in a cost-effective manner.
In this paper, we present infrastructure for developing and verifying impure functional programs with I/O and imperative file handling. Specifically, we extend CakeML with a low-level model of file I/O, and verify a high-level file I/O library in terms of the model. We use this library to develop and verify several Unix-style command-line utilities: cat, sort, grep, diff and patch. The workflow we present is built around the HOL4 theorem prover, and therefore all our results have machine-checked proofs.
This is a preview of subscription content, log in via an institution.
Buying options
Tax calculation will be finalised at checkout
Purchases are for personal use only
Learn about institutional subscriptionsNotes
- 1.
- 2.
- 3.
- 4.
- 5.
The LCS is not always unique: both [a, c] and [b, c] are LCSes of [a, b, c] and [b, a, c].
- 6.
There are algorithms that do better than quadratic time for practically interesting special cases [3]; we leave their verification for future work.
- 7.
Note that this differs from the default behaviour of the GNU implementation of diff, which uses heuristics that do not compute the minimal list if doing so would be prohibitively expensive.
- 8.
The verification is described in more detail in a blog post by Yannick Moy: https://blog.adacore.com/formal-verification-of-legacy-code.
- 9.
@ appends lists.
- 10.
Finding a termination proof for the kind of Brzozowski derivation we use is an open problem that is not addressed by Slind’s work nor by the present paper. See, e.g., Nipkow and Traytel [27] for a discussion.
References
Amani, S., et al.: Cogent: verifying high-assurance file system implementations. In: Conte, T., Zhou, Y. (eds.) Proceedings of the Twenty-First International Conference on Architectural Support for Programming Languages and Operating Systems, ASPLOS 2016, Atlanta, GA, USA, 2–6 April 2016, pp. 175–188. ACM (2016). https://doi.org/10.1145/2872362.2872404
Anand, A., et al.: CertiCoq: a verified compiler for Coq. In: Coq for Programming Languages (CoqPL) (2017)
Apostolico, A., Galil, Z. (eds.): Pattern Matching Algorithms. Oxford University Press, Oxford (1997)
Appel, A.W.: Verified software toolchain. In: Barthe, G. (ed.) ESOP 2011. LNCS, vol. 6602, pp. 1–17. Springer, Heidelberg (2011). https://doi.org/10.1007/978-3-642-19718-5_1
Arkoudas, K., Zee, K., Kuncak, V., Rinard, M.: Verifying a file system implementation. In: Davies, J., Schulte, W., Barnett, M. (eds.) ICFEM 2004. LNCS, vol. 3308, pp. 373–390. Springer, Heidelberg (2004). https://doi.org/10.1007/978-3-540-30482-1_32
Bulwahn, L., Krauss, A., Haftmann, F., Erkök, L., Matthews, J.: Imperative functional programming with Isabelle/HOL. In: Mohamed, O.A., Muñoz, C., Tahar, S. (eds.) TPHOLs 2008. LNCS, vol. 5170, pp. 134–149. Springer, Heidelberg (2008). https://doi.org/10.1007/978-3-540-71067-7_14
Charguéraud, A.: Characteristic formulae for the verification of imperative programs. In: Proceeding of the 16th ACM SIGPLAN International Conference on Functional Programming, ICFP 2011, pp. 418–430 (2011). https://doi.org/10.1145/2034773.2034828
Chlipala, A., et al.: The end of history? Using a proof assistant to replace language design with library design. In: Summit on Advances in Programming Languages (SNAPL). Schloss Dagstuhl - Leibniz-Zentrum fuer Informatik (2017). https://doi.org/10.4230/LIPIcs.SNAPL.2017.3
Delaware, B., Pit-Claudel, C., Gross, J., Chlipala, A.: Fiat: deductive synthesis of abstract data types in a proof assistant. In: Principles of Programming Languages (POPL), pp. 689–700. ACM (2015). https://doi.org/10.1145/2676726.2677006
Ernst, G., Schellhorn, G., Haneberg, D., Pfähler, J., Reif, W.: Verification of a virtual filesystem switch. In: Cohen, E., Rybalchenko, A. (eds.) VSTTE 2013. LNCS, vol. 8164, pp. 242–261. Springer, Heidelberg (2014). https://doi.org/10.1007/978-3-642-54108-7_13
Filliâtre, J.-C., Paskevich, A.: Why3—where programs meet provers. In: Felleisen, M., Gardner, P. (eds.) ESOP 2013. LNCS, vol. 7792, pp. 125–128. Springer, Heidelberg (2013). https://doi.org/10.1007/978-3-642-37036-6_8
Glondu, S.: Vers une certification de lextraction de Coq. Ph.D. thesis, Universit Paris Diderot (2012)
Guéneau, A., Myreen, M.O., Kumar, R., Norrish, M.: Verified characteristic formulae for CakeML. In: Yang, H. (ed.) ESOP 2017. LNCS, vol. 10201, pp. 584–610. Springer, Heidelberg (2017). https://doi.org/10.1007/978-3-662-54434-1_22
Heisel, M.: Specification of the Unix file system: a comparative case study. In: Alagar, V.S., Nivat, M. (eds.) AMAST 1995. LNCS, vol. 936, pp. 475–488. Springer, Heidelberg (1995). https://doi.org/10.1007/3-540-60043-4_72
Ho, S., Abrahamsson, O., Kumar, R., Myreen, M.O., Tan, Y.K., Norrish, M.: Proof-producing synthesis of CakeML with I/O and local state from monadic HOL functions. In: International Joint Conference on Automated Reasoning (IJCAR) (2018, to appear)
Hobor, A.: Oracle Semantics. Princeton University, Princeton (2008)
IEEE Computer Society, The Open Group: The open group base specifications issue 7. IEEE Std 1003.1, 2016 Edition (2016)
Jeannerod, N., Marché, C., Treinen, R.: A formally verified interpreter for a shell-like programming language. In: Paskevich, A., Wies, T. (eds.) VSTTE 2017. LNCS, vol. 10712, pp. 1–18. Springer, Cham (2017). https://doi.org/10.1007/978-3-319-72308-2_1
Khanna, S., Kunal, K., Pierce, B.C.: A formal investigation of Diff3. In: Arvind, V., Prasad, S. (eds.) FSTTCS 2007. LNCS, vol. 4855, pp. 485–496. Springer, Heidelberg (2007). https://doi.org/10.1007/978-3-540-77050-3_40
Kumar, R., Myreen, M.O., Norrish, M., Owens, S.: CakeML: a verified implementation of ML. In: POPL 2014: Proceedings of the 41st ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pp. 179–191. ACM Press, January 2014
Lammich, P.: Refinement to Imperative/HOL. In: Urban, C., Zhang, X. (eds.) ITP 2015. LNCS, vol. 9236, pp. 253–269. Springer, Cham (2015). https://doi.org/10.1007/978-3-319-22102-1_17
Leino, K.R.M.: Dafny: an automatic program verifier for functional correctness. In: Clarke, E.M., Voronkov, A. (eds.) LPAR 2010. LNCS (LNAI), vol. 6355, pp. 348–370. Springer, Heidelberg (2010). https://doi.org/10.1007/978-3-642-17511-4_20
Leroy, X.: A formally verified compiler back-end. J. Autom. Reason. 43(4), 363–446 (2009)
Letouzey, P.: Extraction in Coq: an overview. In: Beckmann, A., Dimitracopoulos, C., Löwe, B. (eds.) CiE 2008. LNCS, vol. 5028, pp. 359–369. Springer, Heidelberg (2008). https://doi.org/10.1007/978-3-540-69407-6_39
McCormick, J.W.: Building High Integrity Applications with Spark ADA. Cambridge University Press, Cambridge (2015)
Myreen, M.O., Owens, S.: Proof-producing translation of higher-order logic into pure and stateful ML. J. Funct. Program. 24(2–3), 284–315 (2014)
Nipkow, T., Traytel, D.: Unified decision procedures for regular expression equivalence. In: Klein, G., Gamboa, R. (eds.) ITP 2014. LNCS, vol. 8558, pp. 450–466. Springer, Cham (2014). https://doi.org/10.1007/978-3-319-08970-6_29
Ntzik, G., Gardner, P.: Reasoning about the POSIX file system: local update and global pathnames. In: Aldrich, J., Eugster, P. (eds.) Proceedings of the 2015 ACM SIGPLAN International Conference on Object-Oriented Programming, Systems, Languages, and Applications, OOPSLA 2015, part of SPLASH 2015, Pittsburgh, PA, USA, 25–30 October 2015, pp. 201–220. ACM (2015). https://doi.org/10.1145/2814270.2814306
Owens, S., Reppy, J.H., Turon, A.: Regular-expression derivatives re-examined. J. Funct. Program. 19(2), 173–190 (2009). https://doi.org/10.1017/S0956796808007090
Ridge, T., Sheets, D., Tuerk, T., Giugliano, A., Madhavapeddy, A., Sewell, P.: SibyLFS: formal specification and oracle-based testing for POSIX and real-world file systems. In: Miller, E.L., Hand, S. (eds.) Proceedings of the 25th Symposium on Operating Systems Principles, SOSP 2015, Monterey, CA, USA, 4–7 October 2015, pp. 38–53. ACM (2015). https://doi.org/10.1145/2815400.2815411
Slind, K.L.: High performance regular expression processing for cross-domain systems with high assurance requirements. Presented at the Third Workshop on Formal Methods And Tools for Security (FMATS3) (2014)
Swamy, N., et al.: Dependent types and multi-monadic effects in F*. In: 43rd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL), pp. 256–270. ACM, January 2016. https://www.fstar-lang.org/papers/mumon/
Tan, Y.K., Myreen, M.O., Kumar, R., Fox, A., Owens, S., Norrish, M.: A new verified compiler backend for CakeML. In: ICFP 2016: Proceedings of the 21th ACM SIGPLAN International Conference on Functional Programming, pp. 60–73. ACM Press, September 2016
Kosmatov, N., Marché, C., Moy, Y., Signoles, J.: Static versus dynamic verification in Why3, Frama-C and SPARK 2014. In: Margaria, T., Steffen, B. (eds.) ISoLA 2016. LNCS, vol. 9952, pp. 461–478. Springer, Cham (2016). https://doi.org/10.1007/978-3-319-47166-2_32
Acknowledgements
The first and fourth authors were supported by EPSRC Grant EP/N028759/1, UK. The second and fifth authors were partly supported by the Swedish Research Council. We would also like to thank the anonymous reviewers for their constructive and insightful comments and corrections.
Author information
Authors and Affiliations
Corresponding authors
Editor information
Editors and Affiliations
A Appendix: Further Example Programs
A Appendix: Further Example Programs
For the benefit of readers, we describe our verified implementations of the grep, sort, and cat command-line utilities.
1.1 A.1 Cat
A verified cat implementation was presented in our previous work on CF [13]. The cat implementation presented here differs in two respects: first, it is verified with respect to a significantly more low-level file system model (see Sect. 3.1). Second, it has significantly improved performance, since it is implemented in terms of more low-level I/O primitives. Hence this example demonstrates that reasonably performant I/O verified with respect to a low-level I/O model is feasible in our setting. Here is the code:
The difference over the previous implementation is pipe_2048, which gains efficiency by requesting 2048 characters at a time from the input stream, rather than single characters as previously. We elide its straightforward CF specification, which essentially states that the output produced on stdout is the concatenation of the file contents of the filenames given as command line arguments. The cat implementation above does not handle exceptions thrown by TextIO.openIn; hence the specification assumes that all command line arguments are valid names of existing files.
1.2 A.2 Sort
The sort program reads all of the lines in from a list of files given on the command-line, puts the lines into an array, sorts them using Quicksort, and then prints out the contents of the array. The proof that the printed output contains all of the lines of the input files, and in sorted order, is tedious, but straightforward.
We do not use an existing Quicksort implementation, but write and verify one from scratch. Unlike the various list-based Quicksort algorithms found in HOL, Coq, and Isabelle, we want an efficient array-based implementation of pivoting. Hence we implement something more akin to Hoare’s original algorithm. We sweep two pointers inward from the start and end of the array, swapping elements when they are on the wrong side of the pivot. We stop when the pointers pass each other. Note that we pass in a comparison function: our Quicksort is parametric in the type of array elements.
Because this is intrinsically imperative code, we do not use the synthesis tool, but instead verify it with CF directly. The only tricky thing about the proof is working out the invariants for the various recursive functions, which are surprisingly subtle, for an algorithm so appealingly intuitive.
Our approach to verifying the algorithm is to assume a correspondence between the CakeML values in the array, and HOL values that have an appropriate ordering on them. The Quicksort algorithm needs that ordering to be a strict weak order. This is a less restrictive assumption than requiring it to be a linear order (strict or otherwise). Roughly speaking, this will allow us to assume that unrelated elements are equivalent, even when they are not equal. Hence, we can sort arrays that hold various kinds of key/value pairs, where there are duplicate keys which might have different values.
Even though we are not using the synthesis tool, we do use its refinement invariant combinators to maintain the CakeML/HOL correspondence. This enforces a mild restriction that our comparison function must be pure, but greatly simplifies the proof by allowing us to reason about ordering and permutation naturally in HOL.
The following is our correctness theorem for partition. We assume that there is a strick weak order \({\textsf {cmp}}\) that corresponds to the CakeML value passed in as the comparison. We also assume some arbitrary refinement invariant \( a \) on the elements of the array. The combinator lifts refinement invariants to functions.
We can read the above as follows, starting in the conclusion of the theorem. Partition takes 5 arguments \( cmp\texttt {\_}{}v \), \( arr\texttt {\_}{}v \), \( pivot\texttt {\_}{}v \), \( lower\texttt {\_}{}v \), and \( upper\texttt {\_}{}v \), all of which are CakeML values. As a precondition, the array’s contents can be split into 3 lists of CakeML values \( elems\texttt {\_}{}vs_{\mathrm {1}} \), \( elems\texttt {\_}{}vs_{\mathrm {2}} \), and \( elems\texttt {\_}{}vs_{\mathrm {3}} \).Footnote 9 Now looking at the assumptions, the length of \( elem\texttt {\_}{}vs_{\mathrm {1}} \) must be the integer value for the lower pointer. A similar relation must hold for the upper pointer, so that \( elem\texttt {\_}{}vs_{\mathrm {2}} \) is the list of elements in-between the pointers, inclusive. We also must assume that the pivot element is in segment to be partitioned (excluding the last element).
The postcondition states that the partition code will terminate, and that there exists two partitions. The array in the heap now contains the two partitions instead of \( elem\texttt {\_}{}vs_{\mathrm {2}} \). The \({\textsf {partition\_pred}}\) predicate (definition omitted), ensures that the two partitions are non-empty, permute \( elem\texttt {\_}{}vs_{\mathrm {2}} \), and that the elements of the first are not greater than the pivot, and the elements of the second are not less. These last two points use the shallowly embedded \({\textsf {cmp}}\) and \( elems_{\mathrm {2}} \), rather than \( cmp\texttt {\_}{}v \) and \( elems\texttt {\_}{}vs_{\mathrm {2}} \).
1.3 A.3 grep
grep prints to stdout every line from the files that matches the regular expression . Unlike sort, diff and patch which need to see the full file contents before producing output, grep can process lines one at a time and produce output after each line. The main loop of grep reads a line, and prints it if it satisfies the predicate m:
For each filename, we run the above loop if the file can be opened, and print an appropriate error message to stderr otherwise:
The latter function satisfies the following CF specification (eliding stderr output):
The postcondition states that the output to stdout is precisely those lines in \( f \) that satisfy \( m \), with \( f \) and a colon prepended to each line. The three assumptions mean, respectively: that \( f \) is a string without null characters, and \( fv \) is its corresponding deeply embedded CakeML value; that our view of the file system has a free file descriptor; and that \( m \) is a fully specified (i.e., lacking preconditions) function of type \(\texttt {char}\;\texttt {lang}\) and \( mv \) is the corresponding CakeML closure value.
The main function of grep is as follows:
parse_regexp and build_matcher are synthesised from a previous formalisation of regular expressions by Slind [31], based on Brzozowski derivatives [29].
The semantics of grep is given by the function \({\textsf {grep\_sem}}\), which returns a tuple of output for stdout and stderr, respectively.
regexp_lang is a specification of build_matcher due to Slind, and grep_sem_file is a semantics definition for print_matching_lines_in_file. The final CF specification states that the output to the std* streams are as in \({\textsf {grep\_sem}}\), and has two premises: that there is an unused file descriptor, and that Brzozowski derivation terminates on the given regular expressionFootnote 10.
Rights and permissions
Copyright information
© 2018 Springer Nature Switzerland AG
About this paper
Cite this paper
Férée, H., Åman Pohjola, J., Kumar, R., Owens, S., Myreen, M.O., Ho, S. (2018). Program Verification in the Presence of I/O. In: Piskac, R., Rümmer, P. (eds) Verified Software. Theories, Tools, and Experiments. VSTTE 2018. Lecture Notes in Computer Science(), vol 11294. Springer, Cham. https://doi.org/10.1007/978-3-030-03592-1_6
Download citation
DOI: https://doi.org/10.1007/978-3-030-03592-1_6
Published:
Publisher Name: Springer, Cham
Print ISBN: 978-3-030-03591-4
Online ISBN: 978-3-030-03592-1
eBook Packages: Computer ScienceComputer Science (R0)