แอปทำงานได้บน Android 9 และตายบนโทรศัพท์เครื่องเดิมหลังอัปเดตเป็น Android 10 โปรไฟล์ของคุณไม่ถูกแตะต้อง ใบรับรองยังเหมือนเดิม และการแครชเกิดก่อนทราฟฟิกการยืนยันตัวตนใด ๆ logcat แสดงพอยน์เตอร์ว่างก่อน และไม่กี่บรรทัดเหนือขึ้นไปมี Cannot run program ...: error=13, Permission denied ข้อความนั้นพูดถึงการรันไฟล์ ไม่ใช่การอ่านคอนฟิกของคุณ

สิ่งที่ข้อผิดพลาด 13 ปฏิเสธจริง ๆ

Android 10 ระดับ API 29 หยุดไม่ให้แอปรันไฟล์ที่ตัวเองเขียนลงไดเรกทอรีข้อมูลของตัวเอง กฎนี้เล็งไปที่ที่เก็บที่เขียนได้แล้วจึงรันได้ ซึ่งเป็นสิ่งที่ตัวเรียกที่คัดลอกจาก assets สร้างขึ้นพอดี บิลด์เก่าของแกน ics-openvpn จะแตกไฟล์ที่รันได้ openvpn ออกจาก APK ในการรันครั้งแรกและเริ่มมันด้วย ProcessBuilder การแตกไฟล์ยังสำเร็จอยู่ สิ่งที่ถูกปฏิเสธคือการรัน แอปที่เล็ง API 29 สืบทอดกฎนี้แม้จะทำงานบนอุปกรณ์ที่ออกมาพร้อมเวอร์ชันเก่ากว่า จึงทำให้การอัปเดตหนึ่งครั้งทำลายบิลด์ที่ไม่มีใครแตะต้องได้

ไม่มีอะไรในการปฏิเสธที่เฉพาะเจาะจงกับชื่อโฟลเดอร์ใดโฟลเดอร์หนึ่ง ไฟล์ที่แอปสร้างขึ้นใต้ไดเรกทอรีข้อมูลของมันถูกถือว่ารันไม่ได้ไม่ว่าเส้นทางจะเป็นอย่างไร ดังนั้น getFilesDir(), getCacheDir() และไดเรกทอรีย่อยที่กำหนดเองก็ล้มเหลวเหมือนกันทั้งหมด

แยกการแครชของตัวช่วยออกจากปัญหาโปรไฟล์

อ่านล็อกรอบ ๆ การเริ่มโปรเซสแทนการเลื่อนจากด้านบน ลายเซ็นที่ต้องค้นหาคือบรรทัด error=13 และสแต็กเทรซด้านบนมักระบุชื่อเมธอดของตัวเรียกที่เรียก ProcessBuilder:

adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"

ถ้าตัวช่วยไม่เคยเริ่มเลย คุณจะไม่เห็นเอาต์พุตล็อกของ OpenVPN เลย ไม่มีการเจรจาไซเฟอร์ ไม่มีเส้นทางที่ถูกดัน ไม่มีการตั้งค่า DNS ถ้าบรรทัดเหล่านั้นปรากฏและความล้มเหลวมาทีหลัง ไบนารีทำงานแล้วและปัญหาอยู่ที่โปรไฟล์ เซิร์ฟเวอร์ หรือชั้นการกำหนดเส้นทาง ความแตกต่างเพียงข้อเดียวนี้ประหยัดเวลามหาศาลที่จะเสียไปกับการแก้ไขไฟล์ .ovpn ที่ไม่เคยเป็นความผิดเลย มันยังบอกด้วยว่าต้องการการแก้ไขใดจากสองข้อด้านล่างจริง ๆ

ตรวจสอบระดับ API บนอุปกรณ์ที่คุณทดสอบด้วย ไม่ใช่ระดับในคอนฟิกบิลด์ของคุณ อิมเมจอีมูเลเตอร์บน API 28 ไม่จำลองอะไรเลย เพราะข้อจำกัดนี้กัดตั้งแต่ 29 ขึ้นไปเท่านั้น ตรวจสอบ SDK เป้าหมายในไฟล์บิลด์ของคุณพร้อมกัน การยกระดับมันคือสิ่งที่ดึงกฎนี้เข้ามา

adb shell getprop ro.build.version.sdk

ทำไมการคัดลอกไบนารีไปที่อื่นจึงไม่ช่วย

ทางอ้อมที่เห็นได้ชัด คือการเขียนตัวช่วยลงไดเรกทอรีอื่นที่เขียนได้ ล้มเหลวด้วยเหตุผลเดียวกับต้นฉบับ Android ยังอนุญาตให้รันไลบรารีที่ใช้ร่วมกันซึ่งติดตั้งไว้ในไดเรกทอรีไลบรารีเนทีฟของแอป เพราะตัวติดตั้งเป็นผู้แพ็กเกจ ไม่ใช่เขียนตอนรัน ไดเรกทอรีนั้นคือที่เดียวที่ตัวเรียกควรชี้ไป สิ่งใดก็ตามที่คุณแตกไฟล์เองตอนรัน ไม่ว่าจะเรียกชื่ออะไร ก็ยังอยู่ในหมวดที่ถูกปฏิเสธ

ที่เก็บภายนอกแย่กว่า เพราะเมานต์ด้วย noexec และตัวช่วยที่วางไว้ที่นั่นไม่สามารถเริ่มได้โดยแอปใด ๆ การล้างข้อมูลแอปก็ไม่คืนพฤติกรรมเดิม เพราะการรันครั้งถัดไปเพียงสร้างสำเนาที่รันไม่ได้เดิมขึ้นใหม่

ชี้ตัวเรียกไปที่ไลบรารีที่แพ็กเกจแล้ว

เปลี่ยนชื่อตัวช่วยเป็นชื่อไลบรารีที่ใช้ร่วมกันและส่งมอบภายใต้ jniLibs สำหรับทุก ABI ที่คุณรองรับ จากนั้นให้ตัวเรียกคืนเส้นทางของไลบรารีที่ติดตั้งตั้งแต่ API 28 เป็นต้นไป:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    return new File(context.getApplicationInfo().nativeLibraryDir,
                    "libovpnexec.so").getPath();
}

ไลบรารีต้องถูกแตกออกตอนติดตั้งเพื่อให้เส้นทางนั้นมีไฟล์จริง ดังนั้นตั้งค่า android:extractNativeLibs="true" บนอิลิเมนต์ <application> ถ้าตัวรวมแมนิเฟสต์นำค่าอื่นเข้ามา ให้เพิ่ม tools:replace="android:extractNativeLibs" ข้าง ๆ มัน เอกสารอ้างอิงอิลิเมนต์ application อธิบายว่าแอตทริบิวต์นี้ควบคุมอะไร

ระวังตัวแปรโลคัลที่ซ้ำกันเมื่อคุณแพตช์ตัวเรียกด้วยมือ ซอร์สเก่าประกาศ nativeAPI ในภายหลังในเมธอดเดียวกัน และการประกาศครั้งที่สองเป็นข้อผิดพลาดตอนคอมไพล์ไม่ใช่ตอนรัน บิลด์จึงล้มเหลวอย่างรวดเร็วและเห็นข้อผิดพลาดได้ง่าย รักษารายการ ABI ในบล็อก Gradle ให้ตรงกับโฟลเดอร์ที่คุณเพิ่ม

ทำให้อาร์ติแฟกต์รีลีสตรงกับบิลด์ทดสอบ

APK ดีบักสามารถยกอุโมงค์ได้ในขณะที่ bundle รีลีสยังแครชอยู่ เพราะ bundle แพ็กไลบรารีเนทีฟในรูปแบบที่ต่างออกไปและอาจเสิร์ฟแบบบีบอัดจากอาร์ไคฟ์ ถ้าเฉพาะบิลด์ที่มาจาก bundle ล้มเหลว ให้เพิ่มสิ่งนี้ใน gradle.properties แล้วบิลด์ใหม่:

android.bundle.enableUncompressedNativeLibs=false

จากนั้นยืนยันว่าแพ็กเกจที่ติดตั้งรายงาน ABI ที่คุณคาดไว้ และทำการตรวจซ้ำบนอุปกรณ์ 32 บิตและ 64 บิตหากคุณส่งทั้งสองแบบ ตัวจัดการแพ็กเกจระบุ ABI ที่มันเลือก ดังนั้นอุปกรณ์ที่ถอยกลับไป 32 บิตจะปรากฏที่นี่ไม่ใช่ในรายงานการแครช:

adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbi

ไลบรารีที่หายไปสำหรับ ABI หนึ่งจะสร้างข้อผิดพลาดสิทธิ์เดิมขึ้นใหม่เฉพาะบนอุปกรณ์นั้น ดังนั้นโทรศัพท์ทดสอบที่สำเร็จเครื่องเดียวจึงไม่ใช่หลักฐาน การส่งแฟล็กแมนิเฟสต์ ชื่อไลบรารี และการตั้งค่า bundle ไปด้วยกัน การเปลี่ยนอย่างหนึ่งแล้วลืมอย่างอื่นจะพาคุณกลับไปยังการแครชเดิม การแก้ไขที่ยั่งยืนคือย้ายไปยังรีลีสแกนที่ได้รับการดูแลแทนการแบกแพตช์ไปตลอดกาล

บทความที่เกี่ยวข้อง